From 5fc5bda08887e2a6a6835caab1e94f2bc9517879 Mon Sep 17 00:00:00 2001 From: ety001 Date: Mon, 10 Nov 2025 14:43:26 +0800 Subject: [PATCH 01/14] create a new table to store config info --- db/config/config.json | 4 +-- db/migrations/20251110062951-create-config.js | 35 +++++++++++++++++++ db/models/config.js | 17 +++++++++ 3 files changed, 53 insertions(+), 3 deletions(-) create mode 100644 db/migrations/20251110062951-create-config.js create mode 100644 db/models/config.js diff --git a/db/config/config.json b/db/config/config.json index e3a8cae3..eded0f4d 100644 --- a/db/config/config.json +++ b/db/config/config.json @@ -3,9 +3,7 @@ "use_env_variable": "DATABASE_URL", "operatorsAliases": "0", "dialectOptions": { - "ssl" : { - "rejectUnauthorized": false - } + "ssl": false } }, "staging": { diff --git a/db/migrations/20251110062951-create-config.js b/db/migrations/20251110062951-create-config.js new file mode 100644 index 00000000..e1444738 --- /dev/null +++ b/db/migrations/20251110062951-create-config.js @@ -0,0 +1,35 @@ +module.exports = { + up: async (queryInterface, Sequelize) => { + await queryInterface.createTable('config', { + id: { + type: Sequelize.INTEGER, + allowNull: false, + autoIncrement: true, + primaryKey: true, + }, + c_key: { + type: Sequelize.STRING, + allowNull: false, + unique: true, + }, + c_val: { + type: Sequelize.TEXT, + allowNull: false, + }, + created_at: { + allowNull: false, + type: Sequelize.DATE, + }, + updated_at: { + allowNull: false, + type: Sequelize.DATE, + }, + }); + await queryInterface.addIndex('config', { fields: ['c_key'], unique: true }); + }, + + down: async (queryInterface) => { + await queryInterface.removeIndex('config', ['c_key']); + await queryInterface.dropTable('config'); + }, +}; diff --git a/db/models/config.js b/db/models/config.js new file mode 100644 index 00000000..8115bcff --- /dev/null +++ b/db/models/config.js @@ -0,0 +1,17 @@ +module.exports = (sequelize, DataTypes) => ( + sequelize.define('config', { + c_key: { + type: DataTypes.STRING, + allowNull: false, + unique: true, + }, + c_val: { + type: DataTypes.TEXT, + allowNull: false, + }, + }, { + freezeTableName: true, + underscored: true, + }) +); + From b6ffd637f37211198086949caf4e2cd589469b96 Mon Sep 17 00:00:00 2001 From: ety001 Date: Mon, 10 Nov 2025 14:48:09 +0800 Subject: [PATCH 02/14] getWhiteEmailDomain() finish --- helpers/database.js | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/helpers/database.js b/helpers/database.js index 02ed6e9f..2b111f0d 100644 --- a/helpers/database.js +++ b/helpers/database.js @@ -118,6 +118,25 @@ const deletePhoneRecord = async where => db.phonecode.destroy({ where }); const deleteEmailRecord = async where => db.emailcode.destroy({ where }); +/** + * Get white email domain list from config table + * Returns default ['gmail.com'] if config not found or parse fails + */ +async function getWhiteEmailDomain() { + try { + const config = await db.config.findOne({ + where: { c_key: 'white_email_domain' }, + }); + if (config && config.c_val) { + const domains = JSON.parse(config.c_val); + return Array.isArray(domains) ? domains : ['gmail.com']; + } + return ['gmail.com']; + } catch (error) { + return ['gmail.com']; + } +} + /** * remove user id references * to remove username reserve mechanism @@ -209,4 +228,5 @@ module.exports = { deleteEmailRecord, findLastSendSmsByCountryNumber, countTryNumber, + getWhiteEmailDomain, }; From 850b3959c268151ecaa994a20d6926d3dbf6453a Mon Sep 17 00:00:00 2001 From: ety001 Date: Mon, 10 Nov 2025 17:13:31 +0800 Subject: [PATCH 03/14] remove phone verify logic from backend --- routes/api.js | 12 ++--- routes/apiHandlers.js | 107 +++++++----------------------------------- src/locales/en.json | 8 ++-- src/locales/fr.json | 2 + src/locales/zh.json | 13 +++-- 5 files changed, 37 insertions(+), 105 deletions(-) diff --git a/routes/api.js b/routes/api.js index d539fbe8..3a4c90c9 100644 --- a/routes/api.js +++ b/routes/api.js @@ -88,15 +88,15 @@ router.post( ) ) ); -router.post('/request_sms_new', apiMiddleware(apiHandlers.handleRequestSmsNew)); +// router.post('/request_sms_new', apiMiddleware(apiHandlers.handleRequestSmsNew)); router.post( '/check_email_code', apiMiddleware(apiHandlers.handleConfirmEmailCode) ); -router.post( - '/check_phone_code', - apiMiddleware(apiHandlers.handleConfirmSmsNew) -); +// router.post( +// '/check_phone_code', +// apiMiddleware(apiHandlers.handleConfirmSmsNew) +// ); router.post( '/create_user_new', apiMiddleware(req => @@ -105,8 +105,6 @@ router.post( req.body.recaptcha, req.body.email, req.body.emailCode, - req.body.phoneNumber, - req.body.phoneCode, req.body.username ) ) diff --git a/routes/apiHandlers.js b/routes/apiHandlers.js index 366b0eb8..358880a1 100644 --- a/routes/apiHandlers.js +++ b/routes/apiHandlers.js @@ -1,6 +1,6 @@ const { hash } = require('@steemit/steem-js/lib/auth/ecc'); const crypto = require('crypto'); -const validator = require('validator'); +// const validator = require('validator'); const jwt = require('jsonwebtoken'); const PNF = require('google-libphonenumber').PhoneNumberFormat; const phoneUtil = require('google-libphonenumber').PhoneNumberUtil.getInstance(); @@ -123,6 +123,7 @@ async function finalizeSignup(user, req) { * @returns {Promise} * @throws {ApiError} */ +/* async function handleRequestEmail( ip, recaptcha, @@ -283,6 +284,7 @@ async function handleRequestEmail( return { success: true, token, xref: user.tracking_id }; } +*/ /** * Checks the phone validity and use with the conveyor @@ -909,10 +911,14 @@ async function handleRequestEmailCode(ip, email, log, locale) { field: 'email', }); } - // bad domains check - if (badDomains.includes(email.split('@')[1])) { - logger.warn({ email }, 'error_api_domain_blacklisted'); - return { success: true, token: null, xref: null }; + // white list check + const emailDomain = email.split('@')[1]; + const whiteEmailDomains = await database.getWhiteEmailDomain(); + if (!whiteEmailDomains.includes(emailDomain)) { + throw new ApiError({ + type: 'error_api_email_domain', + field: 'email', + }); } await database.actionLimitNew(ip, 'request_email_code'); @@ -1550,7 +1556,10 @@ async function handleConfirmSmsNew(req) { }); } - req.log.debug({ prefix: req.body.prefix, phoneNumber: req.body.phoneNumber }, 'debug'); + req.log.debug( + { prefix: req.body.prefix, phoneNumber: req.body.phoneNumber }, + 'debug' + ); const countryCode = req.body.prefix.split('_')[1]; if (!countryCode) { throw new ApiError({ @@ -1633,15 +1642,7 @@ async function handleConfirmSmsNew(req) { return { success: true }; } -async function finalizeSignupNew( - ip, - recaptcha, - email, - emailCode, - phoneNumber, - phoneCode, - username -) { +async function finalizeSignupNew(ip, recaptcha, email, emailCode, username) { if (process.env.RECAPTCHA_SWITCH !== 'OFF') { if (!recaptcha) { throw new ApiError({ @@ -1674,13 +1675,6 @@ async function finalizeSignupNew( }); } - if (!phoneNumber) { - throw new ApiError({ - field: 'phone', - type: 'error_api_phone_required', - }); - } - if (!emailCode) { throw new ApiError({ field: 'email_code', @@ -1688,21 +1682,6 @@ async function finalizeSignupNew( }); } - if (!phoneCode) { - throw new ApiError({ - field: 'phone_code', - type: 'error_api_code_required', - }); - } - - const phoneExists = await database.phoneIsInUse(phoneNumber); - if (phoneExists) { - throw new ApiError({ - field: 'phoneNumber', - type: 'error_api_phone_used', - }); - } - const emailIsInUse = await database.emailIsInUse(email); if (emailIsInUse) { throw new ApiError({ @@ -1711,24 +1690,12 @@ async function finalizeSignupNew( }); } - const phoneRecord = await database.findPhoneRecord({ - where: { - phone_number: phoneNumber, - }, - }); const emailRecord = await database.findEmailRecord({ where: { email, }, }); - if (!phoneRecord) { - throw new ApiError({ - field: 'phone_code', - type: 'error_api_unknown_phone_number', - }); - } - if (!emailRecord) { throw new ApiError({ field: 'email_code', @@ -1736,13 +1703,6 @@ async function finalizeSignupNew( }); } - if (phoneRecord.phone_code !== phoneCode) { - throw new ApiError({ - field: 'phone_code', - type: 'error_api_phone_code_invalid', - }); - } - if (emailRecord.email_code !== emailCode) { throw new ApiError({ field: 'email_code', @@ -1780,7 +1740,6 @@ async function finalizeSignupNew( type: 'signup_new', username, email, - phoneNumber, }, process.env.JWT_SECRET ); @@ -1880,34 +1839,6 @@ async function handleCreateAccountNew(req) { field: 'email', }); } - const phoneExists = await database.phoneIsInUse(decoded.phoneNumber); - if (phoneExists) { - throw new ApiError({ - field: 'phoneNumber', - type: 'error_api_phone_used', - }); - } - const phoneRegistered = await services.conveyorCall('is_phone_registered', [ - decoded.phoneNumber, - ]); - if (phoneRegistered) { - throw new ApiError({ - field: 'phoneNumber', - type: 'error_api_phone_used', - }); - } - - // let user = await database.findUser({ - // where: { - // username: decoded.username, - // email: decoded.email, - // phone_number: decoded.phoneNumber, - // }, - // }); - - // if (user) { - // throw new ApiError({ type: 'error_api_user_exist' }); - // } const weightThreshold = 1; const accountAuths = []; @@ -1958,8 +1889,6 @@ async function handleCreateAccountNew(req) { email: decoded.email, email_normalized: normalizeEmail(decoded.email), email_is_verified: true, - phone_number: decoded.phoneNumber, - phone_number_is_verified: true, ip: req.ip, account_is_created: true, status: 'created', @@ -2008,7 +1937,6 @@ async function handleCreateAccountNew(req) { const params = [ decoded.username, { - phone: user.phone_number.replace(/[^+0-9]+/g, ''), email: user.email, }, ]; @@ -2073,7 +2001,6 @@ async function handleCreateAccountNew(req) { try { await database.deleteEmailRecord({ email: user.email }); - await database.deletePhoneRecord({ phone_number: user.phone_number }); } catch (err) { req.log.warn(err, 'remove email or phone code record error'); } @@ -2110,7 +2037,7 @@ async function handleCreateTronAddr() { } module.exports = { - handleRequestEmail, + // handleRequestEmail, handleRequestSms, handleConfirmEmail, handleConfirmSms, diff --git a/src/locales/en.json b/src/locales/en.json index b50aa941..1cc0f5d4 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -36,6 +36,8 @@ "error_api_create_account": "An error occurred while creating the account", "error_api_domain_blacklisted": "This domain name is blacklisted. Please provide another email address", + "error_api_email_domain": + "This email domain is not allowed. Please use an email from an allowed domain", "error_api_email_exists_not": "Email address doesn't exist", "error_api_email_format": "Please provide a valid email address", "error_api_email_length": @@ -181,7 +183,8 @@ "Your email and phone number are needed to check that you're a real person.", "signup_free_tip3": "Insufficient places,please try again later.", "confirmPassword": "Confirm password", - "create_account_and_download_pdf": "Create the account and download the private key file.", + "create_account_and_download_pdf": + "Create the account and download the private key file.", "create_account_tip1": "Click the button below and wait for about 10 seconds. Do not quit the page. Make sure you have internet access.", "create_account_tip2": @@ -204,8 +207,7 @@ "error_api_user_exist": "User existed", "welcome_page_title": "Welcome", "click_here_to_download": "Click here", - "welcome_page_message_1": - " to download the PDF with your private key.", + "welcome_page_message_1": " to download the PDF with your private key.", "welcome_page_message_2": "Please keep it safe, otherwise we cannot recover it for you.Please do not share the file with anyone else.", "welcome_page_message_3": diff --git a/src/locales/fr.json b/src/locales/fr.json index 43a3e1b1..1958afee 100644 --- a/src/locales/fr.json +++ b/src/locales/fr.json @@ -38,6 +38,8 @@ "Une erreur est survenue lors de la création du compte", "error_api_domain_blacklisted": "Ce domaine est sur liste noire, veuillez fournir un autre email", + "error_api_email_domain": + "Ce domaine email n'est pas autorisé. Veuillez utiliser un email d'un domaine autorisé", "error_api_email_exists_not": "L'email n'existe pas", "error_api_email_format": "Veuillez saisir un email valide", "error_api_email_length": diff --git a/src/locales/zh.json b/src/locales/zh.json index 8af7e68b..0d51ab78 100644 --- a/src/locales/zh.json +++ b/src/locales/zh.json @@ -27,6 +27,7 @@ "error_api_create_account": "创建账号时发生错误", "error_api_domain_blacklisted": "此域名已被加入黑名单,请提供其他E-mail地址", + "error_api_email_domain": "此邮箱域名不在允许列表中,请使用允许的邮箱域名", "error_api_email_exists_not": "Email不存在", "error_api_email_format": "请提供一个有效的Email", "error_api_email_length": "邮箱太长,xx.xx@xx.xx 每个xx 20 字符max", @@ -66,7 +67,8 @@ "error_username_required": "请输入你的用户名", "error_username_taken": "你选择的用户名 {username} 已被使用, 请选择其他用户名。", - "error_validation_account_alpha": "用户名只能由小写字母,数字,英文句点和破折号组成。", + "error_validation_account_alpha": + "用户名只能由小写字母,数字,英文句点和破折号组成。", "error_validation_account_dash": "用户名最多含有一个破折号", "error_validation_account_end": "用户名必须以小写字母或数字结尾", "error_validation_account_max": "用户名过长", @@ -75,7 +77,8 @@ "error_validation_account_segment_alpha": "用户名分段只能由小写字母,数字和破折号组成", "error_validation_account_segment_dash": "用户名分段最多含有一个破折号", - "error_validation_account_segment_end": "用户名分段必须以小写字母或数字结尾", + "error_validation_account_segment_end": + "用户名分段必须以小写字母或数字结尾", "error_validation_account_segment_min": "用户名分段过短", "error_validation_account_segment_start": "用户名分段必须以小写字母开头", "finish_text_1": @@ -132,7 +135,8 @@ "confirmPassword": "确认密码", "create_account": "创建账户", "create_account_and_download_pdf": "创建账户并下载密钥文件", - "create_account_tip1": "点击下方按钮后请耐心等待约10秒钟,不要离开网页,并保证网络状况良好", + "create_account_tip1": + "点击下方按钮后请耐心等待约10秒钟,不要离开网页,并保证网络状况良好", "create_account_tip2": "成功创建账户后,将自动为您下载带有私钥的PDF", "create_account_tip3": "请小心保管,丢失无法找回。不要与任何人共享此文件", "create_account_tip4": "此外,成功创建账户后我们会给您发送一封欢迎邮件", @@ -149,8 +153,7 @@ "error_api_user_exist": "用户已存在", "welcome_page_title": "欢迎", "click_here_to_download": "点此下载", - "welcome_page_message_1": - "带有密钥的PDF。", + "welcome_page_message_1": "带有密钥的PDF。", "welcome_page_message_2": "请小心保管,丢失无法找回。不要与任何人共享此文件。", "welcome_page_message_3": From ca4683f788a12e250acfb9ad968bdbcd5ea77877 Mon Sep 17 00:00:00 2001 From: ety001 Date: Mon, 10 Nov 2025 18:26:40 +0800 Subject: [PATCH 04/14] remove phone verify UI part --- routes/apiHandlers.js | 1 - src/components/Form/Signup/UserInfo.js | 300 +------------------------ 2 files changed, 3 insertions(+), 298 deletions(-) diff --git a/routes/apiHandlers.js b/routes/apiHandlers.js index 358880a1..025c7a89 100644 --- a/routes/apiHandlers.js +++ b/routes/apiHandlers.js @@ -7,7 +7,6 @@ const phoneUtil = require('google-libphonenumber').PhoneNumberUtil.getInstance() const needle = require('needle'); const generateCode = require('../src/utils/phone-utils').generateCode; -const badDomains = require('../bad-domains'); const logger = require('../helpers/logger'); const services = require('../helpers/services'); const database = require('../helpers/database'); diff --git a/src/components/Form/Signup/UserInfo.js b/src/components/Form/Signup/UserInfo.js index 0c7c4eaf..486b3fbb 100644 --- a/src/components/Form/Signup/UserInfo.js +++ b/src/components/Form/Signup/UserInfo.js @@ -1,9 +1,7 @@ import React, { PropTypes } from 'react'; -import PhoneInput from 'react-phone-input-2'; -import 'react-phone-input-2/lib/bootstrap.css'; import { FormattedMessage, injectIntl, intlShape } from 'react-intl'; import ReCAPTCHA from 'react-google-recaptcha'; -import { Form, Input, Button, Icon, message, Modal } from 'antd'; +import { Form, Input, Button, Icon, message } from 'antd'; import SendCode from './SendCode'; import apiCall from '../../../utils/api'; import getFingerprint from '../../../../helpers/fingerprint'; @@ -21,22 +19,13 @@ class UserInfo extends React.Component { email_code: null, email_code_sending: false, email_send_code_txt: '', - phone: null, - rawPhone: null, - prefix: null, - phone_code: null, - phone_code_sending: false, - phone_send_code_txt: '', fingerprint: '', query: '', pending_create_user: false, check_username: false, check_email: false, check_email_code: false, - check_phone_code: false, change_locale_to: this.props.locale, - recaptcha_modal_visible: false, - phone_recaptcha: null, }; } @@ -46,50 +35,32 @@ class UserInfo extends React.Component { email_send_code_txt: this.props.intl.formatMessage({ id: 'send_code', }), - phone_send_code_txt: this.props.intl.formatMessage({ - id: 'send_code', - }), }); } componentWillReceiveProps(nextProps) { if (this.props.locale !== nextProps.locale) { const newState = {}; - const { email_code_sending, phone_code_sending } = this.state; + const { email_code_sending } = this.state; if (!email_code_sending) { newState.email_send_code_txt = this.props.intl.formatMessage({ id: 'send_code', }); } - if (!phone_code_sending) { - newState.phone_send_code_txt = this.props.intl.formatMessage({ - id: 'send_code', - }); - } newState.change_locale_to = nextProps.locale; if (Object.keys(newState).length > 0) { this.setState(newState); } - this.clearGoogleRecaptcha(); } } componentWillUnmount() { // remove interval clearInterval(window.email_code_interval); - clearInterval(window.phone_code_interval); - // remove google recaptcha - // this.clearGoogleRecaptcha(); } getBtnStatus = () => { - const { - check_username, - check_email, - check_email_code, - check_phone_code, - rawPhone, - } = this.state; + const { check_username, check_email, check_email_code } = this.state; const recaptcha = window.config.RECAPTCHA_SWITCH !== 'OFF' ? this.props.form.getFieldValue('recaptcha') @@ -98,32 +69,10 @@ class UserInfo extends React.Component { check_username && check_email && check_email_code && - !!rawPhone && - check_phone_code && !!recaptcha ); }; - getPhoneMasks = () => ({ - cn: '... .... ....', - }); - - clearGoogleRecaptcha = () => { - // remove google recaptcha - for ( - let i = document.getElementsByTagName('script').length - 1; - i >= 0; - i -= 1 - ) { - const scriptNode = document.getElementsByTagName('script')[i]; - if (scriptNode.src.includes('recaptcha')) { - scriptNode.parentNode.removeChild(scriptNode); - } - } - delete window.grecaptcha; - delete window.onloadcallback; - }; - validateAccountNameIntl = (rule, value, callback) => { try { accountNameIsValid(value); @@ -241,54 +190,6 @@ class UserInfo extends React.Component { } }; - validatePhoneRequired = (rule, value, callback) => { - const { intl } = this.props; - setTimeout(() => { - if (this.state.rawPhone) { - callback(); - } else { - callback( - intl.formatMessage({ id: 'error_api_phone_required' }) - ); - } - }); - }; - - validatePhoneCode = (rule, value, callback) => { - const { prefix } = this.state; - if (value) { - const { intl, form } = this.props; - if (value.length !== 6) { - this.setState({ - check_phone_code: false, - }); - callback( - intl.formatMessage({ id: 'error_api_phone_code_invalid' }) - ); - return; - } - const phoneNumber = `+${form.getFieldValue('phone')}`; - apiCall('/api/check_phone_code', { code: value, phoneNumber, prefix }) - .then(() => { - this.setState({ - check_phone_code: true, - }); - callback(); - }) - .catch(error => { - this.setState({ - check_phone_code: false, - }); - callback(intl.formatMessage({ id: error.type })); - }); - } else { - this.setState({ - check_phone_code: false, - }); - callback(); - } - }; - SendEmailCode = email => { if (this.state.email_code_sending) return; const { intl, locale } = this.props; @@ -345,77 +246,6 @@ class UserInfo extends React.Component { }); }; - SendPhoneCodeWrapper = () => { - if (!this.state.rawPhone) return; - if (this.state.phone_code_sending) return; - if (window.config.RECAPTCHA_SWITCH !== 'OFF') { - this.setState({ - recaptcha_modal_visible: true, - }); - } else { - this.SendPhoneCode(); - } - }; - - SendPhoneCode = () => { - if (this.state.phone_code_sending) return; - const { intl, locale } = this.props; - const { phone, rawPhone, prefix, phone_recaptcha } = this.state; - this.setState({ - phone_code_sending: true, - }); - apiCall('/api/request_sms_new', { - phoneNumber: rawPhone, - prefix, - locale, - phone_recaptcha, - }) - .then(() => { - this.props.form.setFields({ - phone: { - value: phone, - }, - }); - window.phone_code_count_seconds = 60; - window.phone_code_interval = setInterval(() => { - if (window.phone_code_count_seconds === 0) { - clearInterval(window.phone_code_interval); - this.setState({ - phone_send_code_txt: intl.formatMessage({ - id: 'send_code', - }), - phone_code_sending: false, - }); - return; - } - window.phone_code_count_seconds -= 1; - this.setState({ - phone_send_code_txt: `${ - window.phone_code_count_seconds - } s`, - }); - }, 1000); - }) - .catch(error => { - this.props.form.setFields({ - phone: { - value: phone, - errors: [ - new Error(intl.formatMessage({ id: error.type })), - ], - }, - }); - window.phone_code_count_seconds = 0; - clearInterval(window.phone_code_interval); - this.setState({ - phone_send_code_txt: intl.formatMessage({ - id: 'send_code', - }), - phone_code_sending: false, - }); - }); - }; - handleSubmit = e => { e.preventDefault(); if (this.state.pending_create_user) return; @@ -430,8 +260,6 @@ class UserInfo extends React.Component { : '', email: form.getFieldValue('email'), emailCode: form.getFieldValue('email_code'), - phoneNumber: `+${form.getFieldValue('phone')}`, - phoneCode: form.getFieldValue('phone_code'), username: form.getFieldValue('username'), }; apiCall('/api/create_user_new', data) @@ -450,20 +278,12 @@ class UserInfo extends React.Component { }); }; - hideRecaptchaModal = () => { - this.setState({ - recaptcha_modal_visible: false, - }); - }; - render() { const { form: { getFieldDecorator, getFieldValue }, intl, origin, - countryCode, } = this.props; - const { recaptcha_modal_visible } = this.state; return (
@@ -579,88 +399,6 @@ class UserInfo extends React.Component { )} -

- -

-

- -

- - {getFieldDecorator('phone', { - validateFirst: true, - rules: [ - { - validator: this.validatePhoneRequired, - message: intl.formatMessage({ - id: 'error_phone_required', - }), - }, - ], - })( - { - const prefix = data.dialCode; - const tmpCountryCode = data.countryCode; - this.setState({ - phone, - rawPhone: phone.slice( - data.dialCode.length - ), - prefix: `${prefix}_${tmpCountryCode}`, - }); - }} - /> - )} - - - {getFieldDecorator('phone_code', { - normalize: this.normalizeUsername, - validateFirst: true, - rules: [ - { - required: true, - message: intl.formatMessage({ - id: 'error_api_code_required', - }), - }, - { - validator: this.validatePhoneCode, - }, - ], - })( - - this.SendPhoneCodeWrapper() - } - /> - } - autoComplete="off" - autoCorrect="off" - autoCapitalize="none" - spellCheck="false" - /> - )} - - {window.config.RECAPTCHA_SWITCH !== 'OFF' && (
@@ -728,36 +466,6 @@ class UserInfo extends React.Component { )} - - {recaptcha_modal_visible === true && ( - { - this.captcha = el; - }} - sitekey={window.config.RECAPTCHA_SITE_KEY} - type="image" - size="normal" - hl={ - this.state.change_locale_to === 'zh' - ? 'zh_CN' - : 'en' - } - onChange={recaptcha => { - this.setState({ - phone_recaptcha: recaptcha, - recaptcha_modal_visible: false, - }); - this.SendPhoneCode(); - }} - /> - )} -
); } @@ -770,13 +478,11 @@ UserInfo.propTypes = { setFields: PropTypes.func.isRequired, getFieldValue: PropTypes.func.isRequired, }).isRequired, - countryCode: PropTypes.string, origin: PropTypes.string.isRequired, handleSubmitUserInfo: PropTypes.func.isRequired, }; UserInfo.defaultProps = { - countryCode: '', origin: '', locale: '', }; From 1a5537536c8efa4ae1af838268d67426eeb0b0de Mon Sep 17 00:00:00 2001 From: ety001 Date: Mon, 10 Nov 2025 18:38:14 +0800 Subject: [PATCH 05/14] add global memory cache for config data --- helpers/cache.js | 109 ++++++++++++++++++++++++++++++++++++++++++++ helpers/database.js | 64 ++++++++++++++++++++++---- 2 files changed, 165 insertions(+), 8 deletions(-) create mode 100644 helpers/cache.js diff --git a/helpers/cache.js b/helpers/cache.js new file mode 100644 index 00000000..766a0783 --- /dev/null +++ b/helpers/cache.js @@ -0,0 +1,109 @@ +/** + * Simple global in-memory cache module + * Used to cache config table data and reduce database queries + */ + +class MemoryCache { + constructor() { + this.cache = new Map(); + this.timers = new Map(); // Store timers for automatic cleanup of expired items + } + + /** + * Set cache value + * @param {string} key - Cache key + * @param {*} value - Cache value + * @param {number} ttl - Time to live in seconds, default 300 seconds (5 minutes) + */ + set(key, value, ttl = 300) { + // Clear existing timer for this key if present + if (this.timers.has(key)) { + clearTimeout(this.timers.get(key)); + } + + // Set cache value + const expireAt = Date.now() + ttl * 1000; + this.cache.set(key, { + value, + expireAt, + }); + + // Set automatic cleanup timer + const timer = setTimeout(() => { + this.delete(key); + }, ttl * 1000); + this.timers.set(key, timer); + } + + /** + * Get cache value + * @param {string} key - Cache key + * @returns {*} Cache value, returns undefined if not found or expired + */ + get(key) { + const item = this.cache.get(key); + if (!item) { + return undefined; + } + + // Check if expired + if (Date.now() > item.expireAt) { + this.delete(key); + return undefined; + } + + return item.value; + } + + /** + * Delete cache item + * @param {string} key - Cache key + */ + delete(key) { + this.cache.delete(key); + if (this.timers.has(key)) { + clearTimeout(this.timers.get(key)); + this.timers.delete(key); + } + } + + /** + * Clear all cache + */ + clear() { + // Clear all timers + this.timers.forEach(timer => clearTimeout(timer)); + this.timers.clear(); + this.cache.clear(); + } + + /** + * Check if cache exists and is not expired + * @param {string} key - Cache key + * @returns {boolean} + */ + has(key) { + const item = this.cache.get(key); + if (!item) { + return false; + } + if (Date.now() > item.expireAt) { + this.delete(key); + return false; + } + return true; + } + + /** + * Get cache size + * @returns {number} + */ + size() { + return this.cache.size; + } +} + +// Create global singleton +const globalCache = new MemoryCache(); + +module.exports = globalCache; diff --git a/helpers/database.js b/helpers/database.js index 2b111f0d..bd0833a3 100644 --- a/helpers/database.js +++ b/helpers/database.js @@ -6,6 +6,7 @@ const { Op } = Sequelize; const { ApiError } = require('./errortypes'); const { normalizeEmail } = require('./validator'); +const cache = require('./cache'); /** * Throws if user or ip exceeds number of allowed actions within time period. @@ -119,24 +120,69 @@ const deletePhoneRecord = async where => db.phonecode.destroy({ where }); const deleteEmailRecord = async where => db.emailcode.destroy({ where }); /** - * Get white email domain list from config table - * Returns default ['gmail.com'] if config not found or parse fails + * Get config value from config table (with cache) + * @param {string} key - Config key name + * @param {*} defaultValue - Default value returned if config not found + * @param {number} ttl - Cache expiration time in seconds, default 300 seconds (5 minutes) + * @returns {Promise<*>} Config value */ -async function getWhiteEmailDomain() { +async function getConfigValue(key, defaultValue = null, ttl = 300) { + const cacheKey = `config:${key}`; + + // Try to get from cache first + const cachedValue = cache.get(cacheKey); + if (cachedValue !== undefined) { + return cachedValue; + } + + // Cache miss, query from database try { const config = await db.config.findOne({ - where: { c_key: 'white_email_domain' }, + where: { c_key: key }, }); + + let value = defaultValue; if (config && config.c_val) { - const domains = JSON.parse(config.c_val); - return Array.isArray(domains) ? domains : ['gmail.com']; + // Try to parse JSON, return raw string if parsing fails + try { + value = JSON.parse(config.c_val); + } catch (e) { + value = config.c_val; + } } - return ['gmail.com']; + + // Store in cache + cache.set(cacheKey, value, ttl); + + return value; } catch (error) { - return ['gmail.com']; + // Return default value on query failure, but don't cache error result + return defaultValue; } } +/** + * Clear cache for specified config + * @param {string} key - Config key name + */ +function clearConfigCache(key) { + const cacheKey = `config:${key}`; + cache.delete(cacheKey); +} + +/** + * Get white email domain list from config table + * Returns default ['gmail.com'] if config not found or parse fails + */ +async function getWhiteEmailDomain() { + const domains = await getConfigValue( + 'white_email_domain', + ['gmail.com'], + 300 + ); + return Array.isArray(domains) ? domains : ['gmail.com']; +} + /** * remove user id references * to remove username reserve mechanism @@ -229,4 +275,6 @@ module.exports = { findLastSendSmsByCountryNumber, countTryNumber, getWhiteEmailDomain, + getConfigValue, + clearConfigCache, }; From ef9d3ac6650f55ca21b7c5c9a149e09cb56887ad Mon Sep 17 00:00:00 2001 From: ety001 Date: Mon, 10 Nov 2025 21:29:56 +0800 Subject: [PATCH 06/14] remove apiHandlers unit test --- routes/apiHandlers.test.js | 141 ------------------------------------- 1 file changed, 141 deletions(-) delete mode 100644 routes/apiHandlers.test.js diff --git a/routes/apiHandlers.test.js b/routes/apiHandlers.test.js deleted file mode 100644 index cb62ad13..00000000 --- a/routes/apiHandlers.test.js +++ /dev/null @@ -1,141 +0,0 @@ -describe('request email verification step', () => { - beforeEach(() => { - jest.resetModules(); - }); - - it('should require recaptcha', async () => { - jest.mock('../db/models'); - jest.mock('../helpers/services'); - jest.mock('../helpers/database'); - const apiHandlers = require('./apiHandlers'); - - return apiHandlers - .handleRequestEmail( - 'ip.requires.recaptcha', - null, - 'foo@bar.com', - 'fingerprint', - { query: 'string' }, - 'username', - 'xref', - 'protocol', - 'host' - ) - .catch(err => - expect(err.type).toEqual('error_api_recaptcha_required') - ); - }); - - it('should let a new user request an email verification who has not tried before', async () => { - jest.mock('../db/models'); - jest.mock('../helpers/services'); - jest.mock('../helpers/database'); - const apiHandlers = require('./apiHandlers'); - const mockDb = require('../helpers/database'); - const mockServices = require('../helpers/services'); - - const ret = await apiHandlers.handleRequestEmail( - 'ip.requires.recaptcha', - 'recaptcha', - 'foo@bar.com', - 'fingerprint', - { query: 'string' }, - 'username', - 'xref', - 'protocol', - 'host' - ); - - // Require & test recaptcha - expect(mockServices.recaptchaRequiredForIp.mock.calls).toEqual([ - ['ip.requires.recaptcha'], - ]); - expect(mockServices.verifyCaptcha.mock.calls).toEqual([ - ['recaptcha', 'ip.requires.recaptcha'], - ]); - - // Throttle - expect(mockDb.actionLimit.mock.calls.length).toBe(1); - - // Keep logs - expect(mockDb.logAction.mock.calls).toEqual([ - [ - { - action: 'request_email', - ip: 'ip.requires.recaptcha', - metadata: { email: 'foo@bar.com' }, - }, - ], - ]); - - // We should check to see if the email is in use - expect(mockDb.emailIsInUse.mock.calls).toEqual([['foo@bar.com']]); - - // We should try to find an existing signup record matching this one's email and username. - expect(mockDb.findUser.mock.calls).toEqual([ - [ - { - where: { - email: 'foo@bar.com', - }, - }, - ], - ]); - - // We should create a new user record - expect(mockDb.createUser.mock.calls.length).toEqual(1); - expect(mockDb.createUserMockSave.mock.calls.length).toEqual(1); - - expect(ret.success).toEqual(true); - - return new Promise(resolve => resolve()); - }); - - it('should not allow creating a user record with a username that has already been booked', async () => { - jest.mock('../db/models'); - jest.mock('../helpers/services'); - jest.mock('../helpers/database'); - const apiHandlers = require('./apiHandlers'); - const mockDb = require('../helpers/database'); - - mockDb.emailIsInUse = async () => true; - - await apiHandlers - .handleRequestEmail( - 'ip', - 'recaptcha', - 'foo@bar.com', - 'fingerprint', - { query: 'string' }, - 'username', - 'xref', - 'protocol', - 'host' - ) - .catch(err => expect(err.type).toEqual('error_api_email_used')); - }); - - it('should not allow creating a user record with an email that is recorded in conveyor as having already been registered', async () => { - jest.mock('../db/models'); - jest.mock('../helpers/services'); - jest.mock('../helpers/database'); - const apiHandlers = require('./apiHandlers'); - const mockServices = require('../helpers/services'); - - mockServices.conveyorCall = async () => true; - - await apiHandlers - .handleRequestEmail( - 'ip', - 'recaptcha', - 'foo@bar.com', - 'fingerprint', - { query: 'string' }, - 'username', - 'xref', - 'protocol', - 'host' - ) - .catch(err => expect(err.type).toEqual('error_api_email_used')); - }); -}); From b9004dd31b137d1a94a4f46ef482d7bf8b902eb0 Mon Sep 17 00:00:00 2001 From: ety001 Date: Mon, 10 Nov 2025 23:04:14 +0800 Subject: [PATCH 07/14] display white list of email domain --- helpers/errortypes.js | 8 +++++++- routes/apiHandlers.js | 1 + src/components/Form/Signup/UserInfo.js | 16 +++++++++++++--- src/utils/api.js | 1 + 4 files changed, 22 insertions(+), 4 deletions(-) diff --git a/helpers/errortypes.js b/helpers/errortypes.js index c4ce2276..379ecad8 100644 --- a/helpers/errortypes.js +++ b/helpers/errortypes.js @@ -4,16 +4,22 @@ class ApiError extends Error { field = 'general', status = 400, cause, + data, }) { super(`${field}:${type}`); this.type = type; this.field = field; this.status = status; this.cause = cause; + this.data = data; } toJSON() { - return { type: this.type, field: this.field }; + const result = { type: this.type, field: this.field }; + if (this.data !== undefined) { + result.data = this.data; + } + return result; } } diff --git a/routes/apiHandlers.js b/routes/apiHandlers.js index 025c7a89..168bcc60 100644 --- a/routes/apiHandlers.js +++ b/routes/apiHandlers.js @@ -917,6 +917,7 @@ async function handleRequestEmailCode(ip, email, log, locale) { throw new ApiError({ type: 'error_api_email_domain', field: 'email', + data: { whiteEmailDomains }, }); } diff --git a/src/components/Form/Signup/UserInfo.js b/src/components/Form/Signup/UserInfo.js index 486b3fbb..90f4eaca 100644 --- a/src/components/Form/Signup/UserInfo.js +++ b/src/components/Form/Signup/UserInfo.js @@ -227,12 +227,22 @@ class UserInfo extends React.Component { }, 1000); }) .catch(error => { + let errorMessage = intl.formatMessage({ id: error.type }); + // If email domain error and contains allowed domain list, show domain list + if ( + error.type === 'error_api_email_domain' && + error.data && + error.data.whiteEmailDomains && + Array.isArray(error.data.whiteEmailDomains) && + error.data.whiteEmailDomains.length > 0 + ) { + const domainsList = error.data.whiteEmailDomains.join(', '); + errorMessage = `${errorMessage} (${domainsList})`; + } this.props.form.setFields({ email: { value: email, - errors: [ - new Error(intl.formatMessage({ id: error.type })), - ], + errors: [new Error(errorMessage)], }, }); window.email_code_count_seconds = 0; diff --git a/src/utils/api.js b/src/utils/api.js index 74390277..7525d9fc 100644 --- a/src/utils/api.js +++ b/src/utils/api.js @@ -30,6 +30,7 @@ export default async function apiCall(path, payload, reqType = 'POST') { const error = new Error('ApiError'); error.type = responseData.error.type; error.field = responseData.error.field; + error.data = responseData.error.data; throw error; } return responseData; From f58c76a32877a16dbeb5c76353ac99c0b01bc0c8 Mon Sep 17 00:00:00 2001 From: ety001 Date: Mon, 10 Nov 2025 23:50:25 +0800 Subject: [PATCH 08/14] change google recaptcha to cloudflare turnstile --- .env.example | 6 +- helpers/__mocks__/services.js | 4 +- helpers/getClientConfig.js | 4 +- helpers/recaptcha.js | 39 ----------- helpers/services.js | 35 ++++++---- package.json | 1 - routes/api.js | 2 +- routes/apiHandlers.js | 32 ++++----- src/components/Form/Signup/UserInfo.js | 47 +++++++++----- src/components/Turnstile.js | 90 ++++++++++++++++++++++++++ 10 files changed, 168 insertions(+), 92 deletions(-) delete mode 100644 helpers/recaptcha.js create mode 100644 src/components/Turnstile.js diff --git a/.env.example b/.env.example index 73eab092..4b75b3ec 100644 --- a/.env.example +++ b/.env.example @@ -3,8 +3,6 @@ DATABASE_URL=mysql://username:password@hostname:port/database TWILIO_ACCOUNT_SID=ACCOUNT_ID TWILIO_AUTH_TOKEN=ACCOUNT_TOKEN TWILIO_SERVICE_SID=XXX -RECAPTCHA_SITE_KEY=RECAPTCHA_KEY -RECAPTCHA_SECRET=RECAPTCHA_SECRET CONVEYOR_POSTING_WIF=CONVEYOR_KEY CONVEYOR_USERNAME=CONVEYOR_USERNAME SENDGRID_API_KEY=SENDGRID_API_KEY @@ -21,7 +19,6 @@ INFLUXDB_URL=http://influx.db:8080 SIFTSCIENCE_JS_SNIPPET_KEY= REACT_DISABLE_ACCOUNT_CREATION=false ANALYTICS_UPDATE_SUPERKEY=123456 -RECAPTCHA_SWITCH=ON INTERNAL_API_TOKEN=xxxx PENDING_CLAIMED_ACCOUNTS_THRESHOLD=100 COUNTRY_CODE=cn @@ -33,3 +30,6 @@ HIGH_FREQUENCY_TIME_RANGE=2 HIGH_FREQUENCY_COUNT=10 CREATOR_INFO=steem|steemcurator01|steemcurator02|booming01|booming02|booming03|booming04 GOOGLE_ANALYTICS_ID= +TURNSTILE_SWITCH=ON +TURNSTILE_SITE_KEY=TURNSTILE_SITE_KEY +TURNSTILE_SECRET=TURNSTILE_SECRET diff --git a/helpers/__mocks__/services.js b/helpers/__mocks__/services.js index b7d60c5c..1f4caac9 100644 --- a/helpers/__mocks__/services.js +++ b/helpers/__mocks__/services.js @@ -1,9 +1,9 @@ -const recaptchaRequiredForIp = jest.fn(ip => ip === 'ip.requires.recaptcha'); +const turnstileRequiredForIp = jest.fn(ip => ip === 'ip.requires.turnstile'); const verifyCaptcha = jest.fn(async () => true); module.exports = { ...jest.genMockFromModule('../services'), - recaptchaRequiredForIp, + turnstileRequiredForIp, verifyCaptcha, }; diff --git a/helpers/getClientConfig.js b/helpers/getClientConfig.js index bbbcc58b..726a1bc5 100644 --- a/helpers/getClientConfig.js +++ b/helpers/getClientConfig.js @@ -6,12 +6,12 @@ */ function getClientConfig() { const envVars = [ - 'RECAPTCHA_SITE_KEY', + 'TURNSTILE_SITE_KEY', 'STEEMJS_URL', 'DEFAULT_REDIRECT_URI', 'SIFTSCIENCE_JS_SNIPPET_KEY', 'REACT_DISABLE_ACCOUNT_CREATION', - 'RECAPTCHA_SWITCH', + 'TURNSTILE_SWITCH', 'PENDING_CLAIMED_ACCOUNTS_THRESHOLD', 'CREATOR_INFO', 'GOOGLE_ANALYTICS_ID', diff --git a/helpers/recaptcha.js b/helpers/recaptcha.js deleted file mode 100644 index 1616a95a..00000000 --- a/helpers/recaptcha.js +++ /dev/null @@ -1,39 +0,0 @@ -const reloadRecaptcha = () => { - if (window.config.RECAPTCHA_SWITCH === 'OFF') { - // eslint-disable-next-line no-console - console.log('recaptcha closed'); - return; - } - for ( - let i = document.getElementsByTagName('script').length - 1; - i >= 0; - i -= 1 - ) { - const scriptNode = document.getElementsByTagName('script')[i]; - if (scriptNode.src.includes('recaptcha')) { - scriptNode.parentNode.removeChild(scriptNode); - } - } - delete window.grecaptcha; - - const callbackName = "onloadcallback"; - const lang = typeof window !== "undefined" - && window.recaptchaOptions - && window.recaptchaOptions.lang ? - `&hl=${window.recaptchaOptions.lang}` : - ""; - const url = `https://www.google.com/recaptcha/api.js?onload=${callbackName}&render=explicit${lang}`; - - const newScriptNode = document.createElement("script"); - - newScriptNode.src = url; - newScriptNode.async = 1; - window.onloadcallback = () => { - if (!window.grecaptcha) return; - window.grecaptcha.reset(); - }; - - document.body.appendChild(newScriptNode); -} - -export default reloadRecaptcha; diff --git a/helpers/services.js b/helpers/services.js index 09b428b5..39361ccf 100644 --- a/helpers/services.js +++ b/helpers/services.js @@ -41,7 +41,7 @@ const condenserSecret = getEnv('CREATE_USER_SECRET'); const condenserUrl = getEnv('CREATE_USER_URL'); const conveyorAccount = getEnv('CONVEYOR_USERNAME'); const conveyorKey = getEnv('CONVEYOR_POSTING_WIF'); -const recaptchaSecret = getEnv('RECAPTCHA_SECRET'); +const turnstileSecret = getEnv('TURNSTILE_SECRET'); // const analyticsIpLimitTime = getEnv('ANALYTICS_IP_LIMIT_TIME'); const rpcNode = getEnv('STEEMJS_URL'); @@ -186,19 +186,32 @@ async function conveyorCall(method, params) { } /** - * Verify Google recaptcha. - * @param recaptcha Challenge. + * Verify Cloudflare Turnstile. + * @param turnstileToken Turnstile token. * @param ip Remote addr of client. */ -async function verifyCaptcha(recaptcha, ip) { +async function verifyCaptcha(turnstileToken, ip) { if (DEBUG_MODE) { logger.warn('Verify captcha for %s', ip); } else { - const url = `https://www.google.com/recaptcha/api/siteverify?secret=${recaptchaSecret}&response=${recaptcha}&remoteip=${ip}`; - const response = await (await fetch(url)).json(); + const url = `https://challenges.cloudflare.com/turnstile/v0/siteverify`; + const formData = `secret=${encodeURIComponent( + turnstileSecret + )}&response=${encodeURIComponent( + turnstileToken + )}&remoteip=${encodeURIComponent(ip)}`; + + const response = await (await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + body: formData, + })).json(); + if (!response.success) { - const codes = response['error-codes'] || ['unknown']; - throw new Error(`Captcha verification failed: ${codes.join()}`); + const errors = response['error-codes'] || ['unknown']; + throw new Error(`Turnstile verification failed: ${errors.join()}`); } } } @@ -383,12 +396,12 @@ function locationFromIp(ip) { } /** - * Should recaptcha be required for this IP address? + * Should turnstile be required for this IP address? * * @param {string} ip ip address * @return {boolean} */ -function recaptchaRequiredForIp(ip) { +function turnstileRequiredForIp(ip) { const location = locationFromIp(ip); return location && location.country && location.country.iso_code !== 'CN'; } @@ -584,7 +597,7 @@ module.exports = { gatekeeperMarkSignupCreated, getOverseerStats, locationFromIp, - recaptchaRequiredForIp, + turnstileRequiredForIp, sendApprovalEmail, sendEmail, sendSMS, diff --git a/package.json b/package.json index 69e121a4..44b3545f 100644 --- a/package.json +++ b/package.json @@ -96,7 +96,6 @@ "react-addons-test-utils": "15.3.2", "react-async-script": "0.9.1", "react-dom": "15.3.2", - "react-google-recaptcha": "https://github.com/steemit/react-google-recaptcha.git#2b8d5d7", "react-hot-loader": "4.0.1", "react-intl": "2.4.0", "react-phone-input-2": "^2.13.5", diff --git a/routes/api.js b/routes/api.js index 3a4c90c9..dd39cb3e 100644 --- a/routes/api.js +++ b/routes/api.js @@ -102,7 +102,7 @@ router.post( apiMiddleware(req => apiHandlers.finalizeSignupNew( req.ip, - req.body.recaptcha, + req.body.turnstile, req.body.email, req.body.emailCode, req.body.username diff --git a/routes/apiHandlers.js b/routes/apiHandlers.js index 168bcc60..8d3e00be 100644 --- a/routes/apiHandlers.js +++ b/routes/apiHandlers.js @@ -110,7 +110,7 @@ async function finalizeSignup(user, req) { * * @async * @param {string} ip - * @param {string} recaptcha + * @param {string} turnstile * @param {string} email * @param {string} fingerprint * @param {object} query @@ -125,7 +125,7 @@ async function finalizeSignup(user, req) { /* async function handleRequestEmail( ip, - recaptcha, + turnstile, email, fingerprint, query, @@ -134,22 +134,22 @@ async function handleRequestEmail( protocol, host ) { - const recaptchaRequired = services.recaptchaRequiredForIp(ip); + const turnstileRequired = services.turnstileRequiredForIp(ip); - if (recaptchaRequired && !recaptcha) { + if (turnstileRequired && !turnstile) { throw new ApiError({ type: 'error_api_recaptcha_required', - field: 'recaptcha', + field: 'turnstile', }); } - if (recaptchaRequired) { + if (turnstileRequired) { try { - await services.verifyCaptcha(recaptcha, ip); + await services.verifyCaptcha(turnstile, ip); } catch (cause) { throw new ApiError({ type: 'error_api_recaptcha_invalid', - field: 'recaptcha', + field: 'turnstile', cause, }); } @@ -1073,16 +1073,16 @@ async function handleRequestSmsNew(req) { type: 'signup_free_tip3', }); } - if (process.env.RECAPTCHA_SWITCH !== 'OFF') { - const recaptcha = req.body.phone_recaptcha; - if (!recaptcha) { + if (process.env.TURNSTILE_SWITCH !== 'OFF') { + const turnstile = req.body.phone_turnstile; + if (!turnstile) { throw new ApiError({ field: 'code', type: 'error_api_recaptcha_required', }); } try { - await services.verifyCaptcha(recaptcha, req.ip); + await services.verifyCaptcha(turnstile, req.ip); } catch (cause) { throw new ApiError({ field: 'code', @@ -1642,16 +1642,16 @@ async function handleConfirmSmsNew(req) { return { success: true }; } -async function finalizeSignupNew(ip, recaptcha, email, emailCode, username) { - if (process.env.RECAPTCHA_SWITCH !== 'OFF') { - if (!recaptcha) { +async function finalizeSignupNew(ip, turnstile, email, emailCode, username) { + if (process.env.TURNSTILE_SWITCH !== 'OFF') { + if (!turnstile) { throw new ApiError({ field: 'code', type: 'error_api_recaptcha_required', }); } try { - await services.verifyCaptcha(recaptcha, ip); + await services.verifyCaptcha(turnstile, ip); } catch (cause) { throw new ApiError({ field: 'code', diff --git a/src/components/Form/Signup/UserInfo.js b/src/components/Form/Signup/UserInfo.js index 90f4eaca..2642a0f1 100644 --- a/src/components/Form/Signup/UserInfo.js +++ b/src/components/Form/Signup/UserInfo.js @@ -1,7 +1,7 @@ import React, { PropTypes } from 'react'; import { FormattedMessage, injectIntl, intlShape } from 'react-intl'; -import ReCAPTCHA from 'react-google-recaptcha'; import { Form, Input, Button, Icon, message } from 'antd'; +import Turnstile from '../../Turnstile'; import SendCode from './SendCode'; import apiCall from '../../../utils/api'; import getFingerprint from '../../../../helpers/fingerprint'; @@ -61,15 +61,15 @@ class UserInfo extends React.Component { getBtnStatus = () => { const { check_username, check_email, check_email_code } = this.state; - const recaptcha = - window.config.RECAPTCHA_SWITCH !== 'OFF' - ? this.props.form.getFieldValue('recaptcha') + const turnstile = + window.config.TURNSTILE_SWITCH !== 'OFF' + ? this.props.form.getFieldValue('turnstile') : true; return !( check_username && check_email && check_email_code && - !!recaptcha + !!turnstile ); }; @@ -264,9 +264,9 @@ class UserInfo extends React.Component { }); const { form, intl, handleSubmitUserInfo } = this.props; const data = { - recaptcha: - window.config.RECAPTCHA_SITE_KEY !== '' - ? form.getFieldValue('recaptcha') + turnstile: + window.config.TURNSTILE_SITE_KEY !== '' + ? form.getFieldValue('turnstile') : '', email: form.getFieldValue('email'), emailCode: form.getFieldValue('email_code'), @@ -409,30 +409,43 @@ class UserInfo extends React.Component { )}
- {window.config.RECAPTCHA_SWITCH !== 'OFF' && ( + {window.config.TURNSTILE_SWITCH !== 'OFF' && (
- {getFieldDecorator('recaptcha', { + {getFieldDecorator('turnstile', { rules: [{}], validateTrigger: '', })( - { this.captcha = el; }} sitekey={ - window.config.RECAPTCHA_SITE_KEY + window.config.TURNSTILE_SITE_KEY } - type="image" - size="normal" - hl={ + language={ this.state.change_locale_to === 'zh' - ? 'zh_CN' + ? 'zh-CN' : 'en' } - onChange={() => {}} + onSuccess={token => { + const { form } = this.props; + form.setFields({ + turnstile: { + value: token, + }, + }); + }} + onError={() => { + const { form } = this.props; + form.setFields({ + turnstile: { + value: '', + }, + }); + }} /> )}
diff --git a/src/components/Turnstile.js b/src/components/Turnstile.js new file mode 100644 index 00000000..0b2a3389 --- /dev/null +++ b/src/components/Turnstile.js @@ -0,0 +1,90 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +class Turnstile extends React.Component { + constructor(props) { + super(props); + this.turnstileRef = null; + this.widgetId = null; + this.scriptLoaded = false; + } + + componentDidMount() { + this.loadScript(); + } + + componentWillUnmount() { + if (window.turnstile && this.widgetId !== null) { + window.turnstile.remove(this.widgetId); + } + } + + loadScript() { + if ( + this.scriptLoaded || + document.getElementById('cf-turnstile-script') + ) { + this.initTurnstile(); + return; + } + + const script = document.createElement('script'); + script.id = 'cf-turnstile-script'; + script.src = 'https://challenges.cloudflare.com/turnstile/v0/api.js'; + script.async = true; + script.defer = true; + script.onload = () => { + this.scriptLoaded = true; + this.initTurnstile(); + }; + document.body.appendChild(script); + } + + initTurnstile() { + if (!window.turnstile || !this.turnstileRef) { + return; + } + + const { sitekey, onSuccess, onError, language } = this.props; + + this.widgetId = window.turnstile.render(this.turnstileRef, { + sitekey, + callback: token => { + if (onSuccess) { + onSuccess(token); + } + }, + 'error-callback': () => { + if (onError) { + onError(); + } + }, + language: language || 'auto', + }); + } + + reset() { + if (window.turnstile && this.widgetId !== null) { + window.turnstile.reset(this.widgetId); + } + } + + render() { + return ( +
{ + this.turnstileRef = el; + }} + /> + ); + } +} + +Turnstile.propTypes = { + sitekey: PropTypes.string.isRequired, + onSuccess: PropTypes.func, + onError: PropTypes.func, + language: PropTypes.string, +}; + +export default Turnstile; From 925889ecd5b2e93f19883045588f592c8c70c8d7 Mon Sep 17 00:00:00 2001 From: ety001 Date: Tue, 11 Nov 2025 00:06:04 +0800 Subject: [PATCH 09/14] fix eslint error --- src/components/Turnstile.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/components/Turnstile.js b/src/components/Turnstile.js index 0b2a3389..72d9da5e 100644 --- a/src/components/Turnstile.js +++ b/src/components/Turnstile.js @@ -87,4 +87,10 @@ Turnstile.propTypes = { language: PropTypes.string, }; +Turnstile.defaultProps = { + onSuccess: null, + onError: null, + language: null, +}; + export default Turnstile; From da814b4d38db3cfc6805ea2e665216b42f1bbfaa Mon Sep 17 00:00:00 2001 From: ety001 Date: Tue, 11 Nov 2025 00:29:03 +0800 Subject: [PATCH 10/14] update yarn.lock --- yarn.lock | 6 ------ 1 file changed, 6 deletions(-) diff --git a/yarn.lock b/yarn.lock index 4d06f3c4..40f84556 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8753,12 +8753,6 @@ react-element-to-jsx-string@^3.0.0: stringify-object "^2.3.1" traverse "^0.6.6" -"react-google-recaptcha@https://github.com/steemit/react-google-recaptcha.git#2b8d5d7": - version "0.9.7" - resolved "https://github.com/steemit/react-google-recaptcha.git#2b8d5d722f94da68a32b05bdf3dfe624ea8c7c8f" - dependencies: - prop-types ">=15.5.0" - react-hot-loader@4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/react-hot-loader/-/react-hot-loader-4.0.1.tgz#48284350ae5d7ba07dac872bd5bbc6e477352593" From c6d721edd277a35b31bdf2bbee530b82bc7976b1 Mon Sep 17 00:00:00 2001 From: ety001 Date: Tue, 11 Nov 2025 10:13:13 +0800 Subject: [PATCH 11/14] remove unuse component associate to gogole recaptcha --- src/components/Form/Signup/Email.js | 215 --------------------- src/components/Form/Signup/EmailChinese.js | 148 -------------- src/components/Form/Signup/PhoneNumber.js | 179 ----------------- src/components/Signup.js | 48 ----- 4 files changed, 590 deletions(-) delete mode 100644 src/components/Form/Signup/Email.js delete mode 100644 src/components/Form/Signup/EmailChinese.js delete mode 100644 src/components/Form/Signup/PhoneNumber.js diff --git a/src/components/Form/Signup/Email.js b/src/components/Form/Signup/Email.js deleted file mode 100644 index 1adaa29c..00000000 --- a/src/components/Form/Signup/Email.js +++ /dev/null @@ -1,215 +0,0 @@ -/* eslint-disable react/prop-types */ -import React, { PropTypes } from 'react'; -import { FormattedMessage, injectIntl } from 'react-intl'; -import ReCAPTCHA from 'react-google-recaptcha'; -import { Form, Icon, Input, Button } from 'antd'; -import apiCall from '../../../utils/api'; -import getFingerprint from '../../../../helpers/fingerprint'; -import { - validateEmail, - validateEmailDomain, -} from '../../../../helpers/validator'; - -class Email extends React.Component { - static contextTypes = { - router: PropTypes.shape({}), - }; - - constructor(props) { - super(props); - this.state = { - submitting: false, - fingerprint: '', - query: '', - }; - } - - componentWillMount() { - this.setState({ - fingerprint: JSON.stringify(getFingerprint()), - query: JSON.stringify(this.context.router.location.query), - }); - } - - // eslint-disable-next-line class-methods-use-this - componentWillUnmount() { - for ( - let i = document.getElementsByTagName('script').length - 1; - i >= 0; - i -= 1 - ) { - const scriptNode = document.getElementsByTagName('script')[i]; - if (scriptNode.src.includes('recaptcha')) { - scriptNode.parentNode.removeChild(scriptNode); - } - } - delete window.grecaptcha; - } - - validateRecaptcha = (rule, value, callback) => { - const { intl } = this.props; - if (window.grecaptcha.getResponse() === '') { - window.grecaptcha.execute(); - callback(intl.formatMessage({ id: 'error_recaptcha_required' })); - } else { - callback(); - } - }; - - // since the recaptcha is executed on submit we need the value - // before handling validation otherwise there will be a concurrent value issue - executeRecaptchaAndSubmit = () => { - if (window.grecaptcha.getResponse() === '') { - try { - window.grecaptcha.execute(); - setTimeout(() => { - this.executeRecaptchaAndSubmit(); - }, 100); - } catch (err) { - // Do nothing, it's here to prevent the exception - // where the recpatcha isn't mounted yet. - } - } else { - this.handleSubmit(); - } - }; - - handleSubmit = () => { - const { - form: { validateFieldsAndScroll, setFields }, - onSubmit, - username, - intl, - trackingId, - } = this.props; - const { fingerprint, query } = this.state; - validateFieldsAndScroll((err, values) => { - if (!err) { - apiCall('/api/request_email', { - email: values.email, - fingerprint, - query, - username, - recaptcha: window.grecaptcha.getResponse(), - xref: trackingId, - }) - .then(data => { - this.setState({ submitting: false }); - if (data.success) { - if (onSubmit) { - onSubmit(values, data.token); - } - } - }) - .catch(error => { - this.setState({ submitting: false }); - setFields({ - email: { - value: values.email, - errors: [ - new Error( - intl.formatMessage({ id: error.type }) - ), - ], - }, - }); - window.grecaptcha.reset(); - }); - } else { - this.setState({ submitting: false }); - } - }); - }; - - render() { - const { form: { getFieldDecorator }, intl, email, goBack } = this.props; - return ( -
{ - e.preventDefault(); - if (this.state.submitting) return; - this.setState({ submitting: true }); - this.executeRecaptchaAndSubmit(); - }} - className="signup-form" - > - - {getFieldDecorator('email', { - rules: [ - { - required: true, - message: intl.formatMessage({ - id: 'error_email_required', - }), - }, - { - validator: validateEmail, - message: intl.formatMessage({ - id: 'error_api_email_format', - }), - }, - { - validator: validateEmailDomain, - message: intl.formatMessage({ - id: 'error_api_domain_blacklisted', - }), - }, - ], - initialValue: email, - })( - } - placeholder={intl.formatMessage({ id: 'email' })} - type="email" - /> - )} - - {getFieldDecorator('recaptcha', { - rules: [ - { - validator: this.validateRecaptcha, - message: intl.formatMessage({ - id: 'error_api_recaptcha_required', - }), - }, - ], - validateTrigger: '', - })( - { - this.captcha = el; - }} - sitekey={window.config.RECAPTCHA_SITE_KEY} - type="image" - size="invisible" - onChange={() => {}} - /> - )} -
- - - - {goBack && ( - - - - )} -
- - ); - } -} - -export default Form.create()(injectIntl(Email)); diff --git a/src/components/Form/Signup/EmailChinese.js b/src/components/Form/Signup/EmailChinese.js deleted file mode 100644 index cdb9ec28..00000000 --- a/src/components/Form/Signup/EmailChinese.js +++ /dev/null @@ -1,148 +0,0 @@ -/* eslint-disable react/prop-types */ -import React, { PropTypes } from 'react'; -import { FormattedMessage, injectIntl } from 'react-intl'; -import { Form, Icon, Input, Button } from 'antd'; -import apiCall from '../../../utils/api'; -import getFingerprint from '../../../../helpers/fingerprint'; -import { - validateEmail, - validateEmailDomain, -} from '../../../../helpers/validator'; - -class Email extends React.Component { - static contextTypes = { - router: PropTypes.shape({}), - }; - - constructor(props) { - super(props); - this.state = { - submitting: false, - fingerprint: '', - query: '', - }; - } - - componentWillMount() { - this.setState({ - fingerprint: JSON.stringify(getFingerprint()), - query: JSON.stringify(this.context.router.location.query), - }); - } - - handleSubmit = () => { - const { - form: { validateFieldsAndScroll, setFields }, - onSubmit, - username, - intl, - trackingId, - } = this.props; - const { fingerprint, query } = this.state; - validateFieldsAndScroll((err, values) => { - if (!err) { - apiCall('/api/request_email', { - email: values.email, - fingerprint, - query, - username, - xref: trackingId, - }) - .then(data => { - this.setState({ submitting: false }); - if (data.success) { - if (onSubmit) { - onSubmit(values, data.token); - } - } - }) - .catch(error => { - this.setState({ submitting: false }); - setFields({ - email: { - value: values.email, - errors: [ - new Error( - intl.formatMessage({ id: error.type }) - ), - ], - }, - }); - }); - } else { - this.setState({ submitting: false }); - } - }); - }; - - render() { - const { form: { getFieldDecorator }, intl, email, goBack } = this.props; - return ( -
{ - e.preventDefault(); - if (this.state.submitting) return; - this.setState({ submitting: true }); - this.handleSubmit(); - }} - className="signup-form" - > - - {getFieldDecorator('email', { - rules: [ - { - required: true, - message: intl.formatMessage({ - id: 'error_email_required', - }), - }, - { - validator: validateEmail, - message: intl.formatMessage({ - id: 'error_api_email_format', - }), - }, - { - validator: validateEmailDomain, - message: intl.formatMessage({ - id: 'error_api_domain_blacklisted', - }), - }, - ], - initialValue: email, - })( - } - placeholder={intl.formatMessage({ id: 'email' })} - type="email" - /> - )} - -
- - - - {goBack && ( - - - - )} -
-
- ); - } -} - -export default Form.create()(injectIntl(Email)); diff --git a/src/components/Form/Signup/PhoneNumber.js b/src/components/Form/Signup/PhoneNumber.js deleted file mode 100644 index d0c3c12f..00000000 --- a/src/components/Form/Signup/PhoneNumber.js +++ /dev/null @@ -1,179 +0,0 @@ -/* eslint-disable react/prop-types */ -import React from 'react'; -import { FormattedMessage, injectIntl } from 'react-intl'; -import { Form, Icon, Input, Select, Button } from 'antd'; -import apiCall from '../../../utils/api'; -import countries from '../../../../countries.json'; - -class PhoneNumber extends React.Component { - constructor(props) { - super(props); - this.state = { - submitting: false, - }; - } - - getPrefixDefaultValue = () => { - const { countryCode } = this.props; - if (countryCode) { - const country = countries.find(c => c.iso === countryCode); - if (country) { - return `${country.prefix}_${country.iso}`; - } - } - return undefined; - }; - - handleSubmit = e => { - e.preventDefault(); - if (this.state.submitting) return; - this.setState({ submitting: true }); - const { - form: { validateFieldsAndScroll, setFields }, - token, - onSubmit, - intl, - } = this.props; - validateFieldsAndScroll((err, values) => { - if (!err) { - apiCall('/api/request_sms', { - token, - phoneNumber: values.phoneNumber, - prefix: values.prefix, - }) - .then(data => { - this.setState({ submitting: false }); - if (data.success) { - if (onSubmit) { - onSubmit({ - ...values, - phoneNumberFormatted: data.phoneNumber, - }); - } - } - }) - .catch(error => { - this.setState({ submitting: false }); - if (error.field === 'phoneNumber') { - setFields({ - phoneNumber: { - value: values.phoneNumber, - errors: [ - new Error( - intl.formatMessage({ - id: error.type, - }) - ), - ], - }, - }); - } - if (error.field === 'prefix') { - setFields({ - prefix: { - value: values.prefix, - errors: [ - new Error( - intl.formatMessage({ - id: error.type, - }) - ), - ], - }, - }); - } - }); - } else { - this.setState({ submitting: false }); - } - }); - }; - - render() { - const { - form: { getFieldDecorator }, - intl, - prefix, - phoneNumber, - } = this.props; - - const prefixSelector = getFieldDecorator('prefix', { - rules: [ - { - required: true, - message: intl.formatMessage({ - id: 'error_country_code_required', - }), - }, - ], - initialValue: prefix || this.getPrefixDefaultValue(), - })( - - ); - return ( -
- {prefixSelector} - - {getFieldDecorator('phoneNumber', { - normalize: d => d.replace(/[^0-9+]+/g, ''), - rules: [ - { - required: true, - message: intl.formatMessage({ - id: 'error_phone_required', - }), - }, - { - pattern: /^\+?\d+$/, - message: intl.formatMessage({ - id: 'error_phone_format', - }), - }, - ], - initialValue: phoneNumber, - })( - } - placeholder={intl.formatMessage({ - id: 'phone_number', - })} - type="tel" - /> - )} - -
- - - -
-
- ); - } -} - -export default Form.create()(injectIntl(PhoneNumber)); diff --git a/src/components/Signup.js b/src/components/Signup.js index a83962bc..2e0936b9 100644 --- a/src/components/Signup.js +++ b/src/components/Signup.js @@ -5,9 +5,6 @@ import { FormattedMessage } from 'react-intl'; import { Button, Icon, Popover } from 'antd'; import { CHECKPOINTS } from '../../constants'; import FormSignupUsername from './Form/Signup/Username'; -import FormSignupEmail from './Form/Signup/Email'; -import FormSignupEmailChinese from './Form/Signup/EmailChinese'; -import FormSignupPhoneNumber from './Form/Signup/PhoneNumber'; import FormSignupConfirmPhoneNumber from './Form/Signup/ConfirmPhoneNumber'; import FormSignupUserInfo from './Form/Signup/UserInfo'; import LanguageItem from './LanguageItem'; @@ -498,51 +495,6 @@ class Signup extends Component { />
)} - {step === 'email' && ( -
-

- -

-

- -

- {countryCode !== 'CN' && ( - - )} - {countryCode === 'CN' && ( - - )} -
- )} - {step === 'phoneNumber' && ( -
-

- -

-

- -

- -
- )} {step === 'confirmPhoneNumber' && (

From e8d27052f2f62c332e319172f220f6aae7881ea0 Mon Sep 17 00:00:00 2001 From: ety001 Date: Tue, 11 Nov 2025 12:23:56 +0800 Subject: [PATCH 12/14] more debug info for trunstile; remove tron_data --- helpers/services.js | 46 +++++++++++++++++++++++++++++++++++-------- routes/apiHandlers.js | 27 ------------------------- 2 files changed, 38 insertions(+), 35 deletions(-) diff --git a/helpers/services.js b/helpers/services.js index 39361ccf..e71c4165 100644 --- a/helpers/services.js +++ b/helpers/services.js @@ -194,6 +194,15 @@ async function verifyCaptcha(turnstileToken, ip) { if (DEBUG_MODE) { logger.warn('Verify captcha for %s', ip); } else { + if (!turnstileSecret || turnstileSecret.trim() === '') { + logger.error('TURNSTILE_SECRET is not configured'); + throw new Error('Turnstile secret key is not configured'); + } + + if (!turnstileToken || turnstileToken.trim() === '') { + throw new Error('Turnstile token is required'); + } + const url = `https://challenges.cloudflare.com/turnstile/v0/siteverify`; const formData = `secret=${encodeURIComponent( turnstileSecret @@ -201,17 +210,38 @@ async function verifyCaptcha(turnstileToken, ip) { turnstileToken )}&remoteip=${encodeURIComponent(ip)}`; - const response = await (await fetch(url, { - method: 'POST', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - }, - body: formData, - })).json(); + let response; + try { + const fetchResponse = await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + body: formData, + }); + response = await fetchResponse.json(); + } catch (error) { + logger.error({ error }, 'Failed to verify Turnstile token'); + throw new Error('Failed to verify Turnstile token'); + } if (!response.success) { const errors = response['error-codes'] || ['unknown']; - throw new Error(`Turnstile verification failed: ${errors.join()}`); + logger.warn( + { errorCodes: errors, ip }, + 'Turnstile verification failed' + ); + + // Provide more specific error messages + if (errors.includes('invalid-input-secret')) { + throw new Error( + 'Turnstile verification failed: Invalid secret key. Please check TURNSTILE_SECRET configuration.' + ); + } + + throw new Error( + `Turnstile verification failed: ${errors.join(', ')}` + ); } } } diff --git a/routes/apiHandlers.js b/routes/apiHandlers.js index 8d3e00be..3eb90984 100644 --- a/routes/apiHandlers.js +++ b/routes/apiHandlers.js @@ -1763,7 +1763,6 @@ async function handleCreateAccountNew(req) { xref, locale, activityTags, - tron_bind_data, source, // format: app|tag (eg. condenser|submit_post) } = req.body; // eslint-disable-line camelcase @@ -1776,11 +1775,6 @@ async function handleCreateAccountNew(req) { throw new ApiError({ type: 'error_api_token_required' }); } - if (!tron_bind_data) { - throw new ApiError({ type: 'error_api_tron_bind_data_required' }); - } - const tronBindData = JSON.parse(tron_bind_data); - let decoded; try { @@ -1907,27 +1901,6 @@ async function handleCreateAccountNew(req) { }); } - try { - const updateTronUserResult = await updateTronUser( - decoded.username, - tronBindData - ); - req.log.info( - { decoded, updateTronUserResult }, - 'bind_tron_address_success' - ); - } catch (cause) { - req.log.error( - { decoded, tronBindData, cause }, - 'error_api_bind_tron_addr_failed' - ); - throw new ApiError({ - type: 'error_api_bind_tron_addr_failed', - cause, - status: 500, - }); - } - // try { // await services.gatekeeperMarkSignupCreated(user); // } catch (error) { From e8246f659212d028db19c12340f12d7e1f4e5c20 Mon Sep 17 00:00:00 2001 From: ety001 Date: Tue, 11 Nov 2025 12:26:10 +0800 Subject: [PATCH 13/14] fix eslint error --- routes/apiHandlers.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/routes/apiHandlers.js b/routes/apiHandlers.js index 3eb90984..e93e005d 100644 --- a/routes/apiHandlers.js +++ b/routes/apiHandlers.js @@ -17,7 +17,7 @@ const { isEmail, } = require('../helpers/validator'); const { ApiError } = require('../helpers/errortypes.js'); -const { getTronAccount, updateTronUser } = require('../helpers/tron'); +const { getTronAccount } = require('../helpers/tron'); /** * Verifies that the json webtoken passed was signed by us and From 23d2ebdfc288198a42d5e93861d0ca65eead52d2 Mon Sep 17 00:00:00 2001 From: ety001 Date: Tue, 11 Nov 2025 13:19:51 +0800 Subject: [PATCH 14/14] block + style gmail address --- helpers/validator.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/helpers/validator.js b/helpers/validator.js index f5b5cd72..00ac62ce 100644 --- a/helpers/validator.js +++ b/helpers/validator.js @@ -114,6 +114,10 @@ const accountNameIsValid = name => { }; const isEmail = email => { + // Explicitly reject email addresses containing '+' character + if (email.includes('+')) { + return false; + } const reg = /^[\w]{1,20}([0-9.]{0,10})+[a-zA-Z0-9]{0,20}@[a-zA-Z0-9]{2,20}(?:\.[a-z]{2,20}){1,3}$/; return reg.test(email); };