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/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, + }) +); + 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/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 02ed6e9f..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. @@ -118,6 +119,70 @@ const deletePhoneRecord = async where => db.phonecode.destroy({ where }); const deleteEmailRecord = async where => db.emailcode.destroy({ where }); +/** + * 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 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: key }, + }); + + let value = defaultValue; + if (config && config.c_val) { + // Try to parse JSON, return raw string if parsing fails + try { + value = JSON.parse(config.c_val); + } catch (e) { + value = config.c_val; + } + } + + // Store in cache + cache.set(cacheKey, value, ttl); + + return value; + } catch (error) { + // 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 @@ -209,4 +274,7 @@ module.exports = { deleteEmailRecord, findLastSendSmsByCountryNumber, countTryNumber, + getWhiteEmailDomain, + getConfigValue, + clearConfigCache, }; 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/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..e71c4165 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,62 @@ 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(); + 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 + )}&response=${encodeURIComponent( + turnstileToken + )}&remoteip=${encodeURIComponent(ip)}`; + + 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 codes = response['error-codes'] || ['unknown']; - throw new Error(`Captcha verification failed: ${codes.join()}`); + const errors = response['error-codes'] || ['unknown']; + 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(', ')}` + ); } } } @@ -383,12 +426,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 +627,7 @@ module.exports = { gatekeeperMarkSignupCreated, getOverseerStats, locationFromIp, - recaptchaRequiredForIp, + turnstileRequiredForIp, sendApprovalEmail, sendEmail, sendSMS, 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); }; 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 d539fbe8..dd39cb3e 100644 --- a/routes/api.js +++ b/routes/api.js @@ -88,25 +88,23 @@ 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 => apiHandlers.finalizeSignupNew( req.ip, - req.body.recaptcha, + req.body.turnstile, 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..e93e005d 100644 --- a/routes/apiHandlers.js +++ b/routes/apiHandlers.js @@ -1,13 +1,12 @@ 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(); 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'); @@ -18,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 @@ -111,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 @@ -123,9 +122,10 @@ async function finalizeSignup(user, req) { * @returns {Promise} * @throws {ApiError} */ +/* 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, }); } @@ -283,6 +283,7 @@ async function handleRequestEmail( return { success: true, token, xref: user.tracking_id }; } +*/ /** * Checks the phone validity and use with the conveyor @@ -909,10 +910,15 @@ 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', + data: { whiteEmailDomains }, + }); } await database.actionLimitNew(ip, 'request_email_code'); @@ -1067,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', @@ -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,24 +1642,16 @@ async function handleConfirmSmsNew(req) { return { success: true }; } -async function finalizeSignupNew( - ip, - recaptcha, - email, - emailCode, - phoneNumber, - phoneCode, - 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', @@ -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 ); @@ -1804,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 @@ -1817,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 { @@ -1880,34 +1833,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 +1883,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', @@ -1978,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) { @@ -2008,7 +1910,6 @@ async function handleCreateAccountNew(req) { const params = [ decoded.username, { - phone: user.phone_number.replace(/[^+0-9]+/g, ''), email: user.email, }, ]; @@ -2073,7 +1974,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 +2010,7 @@ async function handleCreateTronAddr() { } module.exports = { - handleRequestEmail, + // handleRequestEmail, handleRequestSms, handleConfirmEmail, handleConfirmSms, 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')); - }); -}); 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 ( -
-
-