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);
});
}
};