Implementing Phone Number and CAPTCHA-Based Authentication in React with Express Backend

Backend API Endpoints

const express = require('express');
const router = express.Router();
const User = require('./../sql/collection/users');
const sql = require('./../sql');
const utils = require('./../utils');
const uuid = require('uuid');
const bcrypt = require('bcryptjs');
const jwt = require('jsonwebtoken');
const saltRounds = 10;
const smsService = require('./../utils/code');

// Login with phone number only (assumes pre-registration)
router.post('/quicklogin', async (req, res) => {
  const { tel } = req.body;
  try {
    const users = await sql.find(User, { tel }, { _id: 0 });
    if (users.length === 0) {
      return res.json({ code: '10086', msg: 'User not registered' });
    }
    const { userid, username } = users[0];
    const token = jwt.sign({ userid }, 'daxunxun', { expiresIn: '7d' });
    res.json({
      code: '10010',
      message: 'Login successful',
      token,
      userid,
      username
    });
  } catch (err) {
    next(err);
  }
});

// Request verification code for quick login
router.post('/quick', async (req, res) => {
  const { tel } = req.body;
  try {
    const users = await sql.find(User, { tel }, { _id: 0 });
    if (users.length === 0) {
      return res.json({ code: '202', msg: 'User not registered' });
    }
    const code = Math.floor(10000 + Math.random() * 90000);
    const result = await smsService.sendCode(tel, code);
    if (result === 1) {
      res.json({ code: '200', msg: 'Verification code sent', data: code });
    } else {
      res.json({ code: '201', msg: 'Failed to send code' });
    }
  } catch (err) {
    next(err);
  }
});

// Send verification code during registration
router.post('/check', async (req, res) => {
  const { tel } = req.body;
  const code = Math.floor(10000 + Math.random() * 90000);
  try {
    const result = await smsService.sendCode(tel, code);
    if (result === 1) {
      res.json({ code: '200', msg: 'Verification code sent', data: code });
    } else {
      res.json({ code: '201', msg: 'Failed to send code' });
    }
  } catch (err) {
    res.json({ code: '201', msg: 'Failed to send code' });
  }
});

// User registration
router.post('/register', async (req, res) => {
  const { username, password, tel } = req.body;
  try {
    const existing = await sql.find(User, { tel }, { _id: 0 });
    if (existing.length > 0) {
      return res.json(utils.registered);
    }
    const userid = `user_${uuid.v1()}`;
    const hashedPwd = bcrypt.hashSync(password, saltRounds);
    await sql.insert(User, { userid, username, password: hashedPwd, tel });
    res.json(utils.registersuccess);
  } catch (err) {
    next(err);
  }
});

// Traditional login with password
router.post('/login', async (req, res) => {
  const { tel, password } = req.body;
  try {
    const users = await sql.find(User, { tel }, { _id: 0 });
    if (users.length === 0) {
      return res.json(utils.unregister);
    }
    const { password: storedPwd, userid, username } = users[0];
    const isValid = bcrypt.compareSync(password, storedPwd);
    if (!isValid) {
      return res.json({ code: '10100', message: 'Incorrect password' });
    }
    const token = jwt.sign({ userid }, 'daxunxun', { expiresIn: '7d' });
    res.json({
      code: '10010',
      message: 'Login successful',
      token,
      userid,
      username
    });
  } catch (err) {
    next(err);
  }
});

module.exports = router;

Password-Based Login Component

import React, { Component } from 'react';
import { Link, withRouter } from 'react-router-dom';
import { login } from '@/utils/api';
import { Toast } from 'antd-mobile';
import './style.scss';

class PasswordLogin extends Component {
  state = { tel: '', password: '' };

  handleLogin = async () => {
    const { tel, password } = this.state;
    if (!tel || !password) {
      return Toast.fail('Please enter phone and password', 2);
    }
    try {
      const resp = await login(tel, password);
      if (resp.code === '10010') {
        localStorage.setItem('token', resp.token);
        localStorage.setItem('username', resp.username);
        localStorage.setItem('userid', resp.userid);
        localStorage.setItem('isLogin', '1');
        Toast.success('Login successful', 1);
        this.props.history.push('/user');
      } else if (resp.code === '10086') {
        Toast.offline('User not registered', 2);
      } else if (resp.code === '10100') {
        Toast.fail('Incorrect password', 2);
      }
    } catch (err) {
      Toast.fail('Network error', 2);
    }
  };

  updatePhone = (e) => this.setState({ tel: e.target.value });
  updatePassword = (e) => this.setState({ password: e.target.value });

  render() {
    return (
      <div className="box">
        <header className="header loginHeader">
          <div className="imgbox">
            <img src="//img.58cdn.com.cn/jxedt/logos/logo3.gif" alt="" />
          </div>
        </header>
        <div className="content loginColor">
          <h2 className="title">Login</h2>
          <div className="loginFrom">
            <p>
              <i className="iconfont icon-shoujihao"></i>
              <input type="text" placeholder="Phone number" onBlur={this.updatePhone} />
            </p>
            <p>
              <i className="iconfont icon-mima"></i>
              <input type="password" placeholder="Password" onBlur={this.updatePassword} />
            </p>
            <div className="loginBtn" onClick={this.handleLogin}>Login</div>
            <Link className="tabQuick" to="/o/quicklogin">Switch to Quick Login</Link>
          </div>
          <div className="noUser">
            Not registered? <Link to="/o/register">Sign up</Link>
          </div>
        </div>
      </div>
    );
  }
}

export default withRouter(PasswordLogin);

CAPTCHA-Based Quick Login

import React, { Component } from 'react';
import { Link, withRouter } from 'react-router-dom';
import { quick, quickLogin } from '@/utils/api';
import { Toast } from 'antd-mobile';
import Cookies from 'js-cookie';
import './style.scss';

class QuickLogin extends Component {
  state = {
    tel: '',
    inputCode: '',
    serverCode: '',
    cooldownActive: false,
    buttonText: 'Get Code',
    countdown: 0
  };

  componentDidMount() {
    const savedCountdown = Cookies.get('sms_cooldown');
    if (savedCountdown) {
      this.startCountdown(Number(savedCountdown));
    }
  }

  startCountdown = (seconds) => {
    this.setState({ cooldownActive: true, countdown: seconds });
    const interval = setInterval(() => {
      this.setState(({ countdown }) => {
        const newCount = countdown - 1;
        if (newCount <= 0) {
          clearInterval(interval);
          Cookies.remove('sms_cooldown');
          return { cooldownActive: false, buttonText: 'Get Code', countdown: 0 };
        }
        Cookies.set('sms_cooldown', newCount, { expires: new Date(Date.now() + newCount * 1000) });
        return { countdown: newCount, buttonText: `Resend (${newCount})` };
      });
    }, 1000);
  };

  requestCode = async () => {
    const { tel } = this.state;
    if (!tel) return Toast.fail('Enter phone number first', 1);
    try {
      const resp = await quick(tel);
      if (resp.code === '200') {
        this.setState({ serverCode: resp.data });
        Cookies.set('sms_cooldown', 60, { expires: new Date(Date.now() + 60000) });
        Toast.success('Code sent successfully', 2);
        this.startCountdown(60);
      } else if (resp.code === '201') {
        Toast.fail('Failed to send code', 2);
      } else {
        Toast.offline('User not registered', 2);
      }
    } catch (err) {
      Toast.fail('Network error', 2);
    }
  };

  attemptLogin = async () => {
    const { tel, inputCode, serverCode } = this.state;
    if (!tel || !inputCode) return Toast.fail('Enter phone and code', 2);
    if (Number(inputCode) !== serverCode) return Toast.fail('Invalid code', 2);
    try {
      const resp = await quickLogin(tel);
      if (resp.code === '10010') {
        localStorage.setItem('token', resp.token);
        localStorage.setItem('userid', resp.userid);
        localStorage.setItem('username', resp.username);
        localStorage.setItem('isLogin', '1');
        Toast.success('Login successful', 2);
        this.props.history.push('/user');
      }
    } catch (err) {
      Toast.fail('Login failed', 2);
    }
  };

  updatePhone = (e) => this.setState({ tel: e.target.value });
  updateInputCode = (e) => this.setState({ inputCode: e.target.value });

  render() {
    return (
      <div className="box">
        <header className="header loginHeader">
          <div className="imgbox">
            <img src="//img.58cdn.com.cn/jxedt/logos/logo3.gif" alt="" />
          </div>
        </header>
        <div className="content loginColor">
          <h2 className="title">Quick Login</h2>
          <div className="loginFrom">
            <p>
              <i className="iconfont icon-shoujihao"></i>
              <input type="text" placeholder="Phone number" onBlur={this.updatePhone} />
            </p>
            <p>
              <i className="iconfont icon-yanzhengma"></i>
              <input type="text" placeholder="Verification code" onBlur={this.updateInputCode} />
              <button
                className="checkBtn"
                disabled={this.state.cooldownActive}
                onClick={this.requestCode}
              >
                {this.state.buttonText}
              </button>
            </p>
            <div className="loginBtn" onClick={this.attemptLogin}>Login</div>
            <Link className="tabLogin" to="/o/login">Switch to Password Login</Link>
          </div>
          <div className="noUser">
            Not registered? <Link to="/o/register">Sign up</Link>
          </div>
        </div>
      </div>
    );
  }
}

export default withRouter(QuickLogin);

Registration with CAPTCHA Verification

import React, { Component } from 'react';
import { Link, withRouter } from 'react-router-dom';
import { getCheck, register } from '@/utils/api';
import { Toast } from 'antd-mobile';
import Cookies from 'js-cookie';
import './style.scss';

class RegisterForm extends Component {
  state = {
    username: '',
    usernameError: '',
    phone: '',
    phoneError: '',
    password: '',
    passwordError: '',
    inputCode: '',
    serverCode: 0,
    cooldownActive: false,
    buttonText: 'Get Code',
    countdown: 0
  };

  componentDidMount() {
    const saved = Cookies.get('reg_sms_cooldown');
    if (saved) this.startCountdown(Number(saved));
  }

  startCountdown = (seconds) => {
    this.setState({ cooldownActive: true, countdown: seconds });
    const interval = setInterval(() => {
      this.setState(({ countdown }) => {
        const newCount = countdown - 1;
        if (newCount <= 0) {
          clearInterval(interval);
          Cookies.remove('reg_sms_cooldown');
          return { cooldownActive: false, buttonText: 'Get Code', countdown: 0 };
        }
        Cookies.set('reg_sms_cooldown', newCount, { expires: new Date(Date.now() + newCount * 1000) });
        return { countdown: newCount, buttonText: `Resend (${newCount})` };
      });
    }, 1000);
  };

  validateUsername = (val) => {
    this.setState({
      username: val,
      usernameError: val && val.length < 2 ? 'Username must be at least 2 characters' : ''
    });
  };

  validatePhone = (val) => {
    const isValid = /^1[34578]\d{9}$/.test(val);
    this.setState({
      phone: val,
      phoneError: val && !isValid ? 'Invalid phone number' : ''
    });
  };

  validatePassword = (val) => {
    const isValid = /^[a-zA-Z][a-zA-Z0-9._]{5,15}$/.test(val);
    this.setState({
      password: val,
      passwordError: val && !isValid
        ? 'Must start with letter, 6–16 chars (letters, digits, ., _)'
        : ''
    });
  };

  requestCode = async () => {
    const { phone } = this.state;
    if (!phone) return Toast.fail('Enter phone number', 1);
    try {
      const resp = await getCheck(phone);
      this.setState({ serverCode: resp.data.data });
      Cookies.set('reg_sms_cooldown', 60, { expires: new Date(Date.now() + 60000) });
      this.startCountdown(60);
    } catch (err) {
      Toast.fail('Failed to send code', 1);
    }
  };

  handleRegister = async () => {
    const { usernameError, phoneError, passwordError, username, phone, password, inputCode, serverCode } = this.state;
    if (usernameError || phoneError || passwordError) {
      return Toast.fail('Fix input errors', 1);
    }
    if (Number(inputCode) !== serverCode) {
      return Toast.fail('Invalid verification code', 1);
    }
    try {
      const resp = await register(phone, username, password);
      if (resp.code === '10000') {
        Toast.info('Already registered. Please log in.', 1);
      } else {
        Toast.success('Registration successful!', 1);
      }
    } catch (err) {
      Toast.fail('Registration failed', 1);
    }
  };

  render() {
    return (
      <div className="box">
        <header className="header registerHeader">
          <div className="imgbox">
            <img src="//img.58cdn.com.cn/jxedt/logos/logo3.gif" alt="" />
          </div>
        </header>
        <div className="content registerColor">
          <h2 className="title">Register</h2>
          <div className="registerFrom">
            <p>
              <i className="iconfont icon-yonghu"></i>
              <input
                type="text"
                placeholder="Username"
                onChange={(e) => this.validateUsername(e.target.value)}
              />
            </p>
            <div className="tip">{this.state.usernameError}</div>
            <p>
              <i className="iconfont icon-shoujihao"></i>
              <input
                type="text"
                placeholder="Phone number"
                onChange={(e) => this.validatePhone(e.target.value)}
              />
            </p>
            <div className="tip">{this.state.phoneError}</div>
            <p>
              <i className="iconfont icon-mima"></i>
              <input
                type="password"
                placeholder="Password"
                onChange={(e) => this.validatePassword(e.target.value)}
              />
            </p>
            <div className="tip">{this.state.passwordError}</div>
            <p>
              <i className="iconfont icon-yanzhengma"></i>
              <input
                type="text"
                placeholder="Verification code"
                onChange={(e) => this.setState({ inputCode: e.target.value })}
              />
              <button
                className="checkBtn"
                disabled={this.state.cooldownActive}
                onClick={this.requestCode}
              >
                {this.state.buttonText}
              </button>
            </p>
            <div className="tip"></div>
            <div className="registerBtn" onClick={this.handleRegister}>Register</div>
          </div>
          <div className="toLogin">
            Already have an account? <Link to="/o/login">Log in</Link>
          </div>
        </div>
      </div>
    );
  }
}

export default withRouter(RegisterForm);

SMS Service Integration

const Core = require('@alicloud/pop-core');

const client = new Core({
  accessKeyId: process.env.ALIYUN_ACCESS_KEY,
  accessKeySecret: process.env.ALIYUN_SECRET_KEY,
  endpoint: 'https://dysmsapi.aliyuncs.com',
  apiVersion: '2017-05-25'
});

module.exports = {
  sendCode(phone, code) {
    const params = {
      RegionId: 'cn-hangzhou',
      PhoneNumbers: phone,
      SignName: process.env.SMS_SIGNATURE,
      TemplateCode: process.env.SMS_TEMPLATE_CODE,
      TemplateParam: JSON.stringify({ code })
    };

    return new Promise((resolve, reject) => {
      client.request('SendSms', params, { method: 'POST' })
        .then(result => resolve(result.Code === 'OK' ? 1 : 0))
        .catch(reject);
    });
  }
};

Tags: React Express Authentication CAPTCHA SMS Verification

Posted on Sat, 07 Nov 2026 16:35:14 +0000 by iversonm