sms.js 10 KB
/* eslint no-unused-vars: ["error", { "args": "none" }] */
'use strict';
const _ = require('lodash');
const helpers = global.yoho.helpers;
const cookie = global.yoho.cookie;
const RegService = require('../models/reg-service');
const PhoneService = require('../models/phone-service');
const AuthHelper = require('../models/auth-helper');
const captchaService = require('../models/captcha-service');

// constrant
const CODE_REQUIRED = '请输入校验码';
const PASSWORD_REQUIRED = '请输入密码';
const PASSWORD_LENGTH_ERROR = '密码6-20位,请重新输入';
const BAD_PASSWORD = '密码格式不正确';
const TOO_MANY = '请求太频繁';
const LOGIN_SUCCSS = '登录成功';
const VERIFY_ERROR = '校验失败';

exports.beforeIn = (req, res, next) => {
    res.set({
        'Cache-Control': 'no-cache, no-store, must-revalidate',
        Pragma: 'no-cache',
        Expires: 0
    });

    if (!req.xhr && req.user.uid) {
        return res.redirect(req.cookies.refer || '/');
    }

    next();
};

// 短信登录 第一步: 展现页面
const _step1 = (req, res, next) => {
    _.set(req.session, 'smsLogin.step', 1);

    if (req.session.smsLogin.count == null) { // eslint-disable-line
        req.session.smsLogin.count = 5;
    }

    let template = 'sms/login';
    let viewData = {
        module: 'passport',
        page: 'sms-login',
        title: '手机短信登录',
        isPassportPage: true,
        headerText: '手机号码快捷登录',
        captchaUrl: helpers.urlFormat('/passport/sms_login/captcha.png', {t: Date.now()}),
        areaCode: '+86', // 默认的区号
        countrys: RegService.getAreaData() // 地区信息列表
    };

    res.render(template, viewData);
};

// 短信登录 第二步: 输入 校验码
const _step2 = (req, res, next) => {
    const mobile = req.session.smsLogin.mobile;
    const area = req.session.smsLogin.area;
    const interval = req.session.smsLogin.interval;

    const template = 'sms/check';
    const viewData = {
        module: 'passport',
        page: 'sms-check',
        title: '手机短信登录',
        isPassportPage: true,
        headerText: '手机号码快捷登录',
        countdown: Math.ceil((interval - Date.now()) / 1000),
        mobile,
        area
    };

    res.render(template, viewData);
};

// 短信登录 第三步: 设置密码 (针对 改手机未注册用户)
const _step3 = (req, res, next) => {
    const template = 'sms/password';
    const viewData = {
        module: 'passport',
        page: 'sms-password',
        title: '设置密码',
        isPassportPage: true,
        headerText: '设置密码'
    };

    res.render(template, viewData);
};

// 短信 登录
exports.loginPage = (req, res, next) => {
    let step = Number(req.query.step) || 1;
    let smsLoginStep = _.get(req.session, 'smsLogin.step', 1);

    if (step === 2 && smsLoginStep !== 2) {
        return res.redirect(req.path);
    }

    if (step === 3 && smsLoginStep !== 3) {
        return res.redirect(req.path);
    }

    switch (step) {
        case 2:
            _step2(req, res, next);
            break;
        case 3:
            _step3(req, res, next);
            break;
        case 1:
        default:
            _step1(req, res, next);
    }
};

exports.tokenBefore = (req, res, next) => {
    let area = req.query.area = (req.query.area || '').trim();
    let mobile = req.query.mobile = (req.query.mobile || '').trim();
    let step = _.get(req.session, 'smsLogin.step');
    let count = _.get(req.session, 'smsLogin.count');
    let interval = _.get(req.session, 'smsLogin.interval');
    let captcha1 = _.get(req.session, 'smsLogin.captcha');
    let captcha2 = (req.query.captcha || '').trim();


    if (!req.xhr && !captcha1) {
        return next(404);
    }

    if ([area, mobile, captcha2].some(val => val === '')) {
        return res.json({
            code: 401,
            message: '请求参数,无法处理'
        });
    }

    delete req.session.smsLogin.captcha; // 图形验证码 一次性

    // step1 要 校验图形验证码
    if (step === 1) {
        if (captcha1 !== captcha2) {
            return res.json({
                code: 400,
                message: VERIFY_ERROR
            });
        }
    }

    let now = Date.now();

    // 重发次数用完了, 回冻结5min
    //      1. 过了冻结期, count 重设为 5次
    //      2. 没过冻结期, end
    // 没有用完, 判断是否请求太频繁
    if (!count) {
        if (interval > now) {
            return res.json({
                code: 400,
                message: TOO_MANY,
                during: Math.ceil((interval - now) / 1000)
            });
        } else {
            _.set(req.session, 'smsLogin.count', 5);
        }
    } else if (interval > now) {
        return res.json({
            code: 429,
            message: TOO_MANY
        });
    }

    next();
};

// AJAX 获取验证码
exports.token = (req, res, next) => {
    let area = req.query.area;
    let mobile = req.query.mobile;

    PhoneService.sendSMS(mobile, area, 1).then(result => {
        if (result.code === 200) {

            _.set(req.session, 'smsLogin.step', 2);
            _.set(req.session, 'smsLogin.area', area);
            _.set(req.session, 'smsLogin.mobile', mobile);

            --req.session.smsLogin.count;

            if (!req.session.smsLogin.count) {
                _.set(req.session, 'smsLogin.interval', Date.now() + 5 * 60 * 1000);
            } else {
                _.set(req.session, 'smsLogin.interval', Date.now() + 60 * 1000);
            }

            result.redirect = '/passport/sms_login?step=2';
            res.json(result);
            return;
        }

        res.json(result);
    });
};

exports.checkBefore = (req, res, next) => {
    let code = req.query.code = (req.query.code || '').trim();
    let step = _.get(req.session, 'smsLogin.step');

    if (!req.xhr && step !== 2) {
        return next(404);
    }

    if (!code) {
        return res.json({
            code: 404,
            message: CODE_REQUIRED
        });
    }

    next();
};

// AJAX 校验验证码 in step2
exports.check = (req, res, next) => {
    const code = req.query.code;
    const mobile = req.session.smsLogin.mobile;
    const area = req.session.smsLogin.area;
    const shopping_key = cookie.getShoppingKey(req); // eslint-disable-line


    Promise.all([
        PhoneService.checkUserPhoneExist(mobile, area),
        PhoneService.verifySMS(mobile, area, code, 1)
    ])
        .then(result => {
            let r1 = result[0] || {};
            let r2 = result[1] || {};
            let redirect;

            // 验证码 校验异常
            if (r2.code !== 200) {
                res.json(r2);
                return;
            }

            // 检测 手机号 是否注册 异常
            if (r1.code !== 200) {
                res.json(r1);
                return;
            }

            // 校验失败
            if (r2.data.is_pass !== 'Y') {
                res.json({
                    code: 401,
                    message: VERIFY_ERROR
                });

                return;
            }

            // 手机号码 没注册
            if (r1.data.is_register !== 'Y') {
                redirect = '/passport/sms_login?step=3';
                _.set(req.session, 'smsLogin.step', 3);

                res.json({
                    code: 200,
                    redirect,
                    newer: true,
                });

                return;
            }

            // 手机号码已注册 --> 直接登录
            PhoneService.autoSignin({
                profile: mobile,
                code: r2.data.code,
                area,
                shopping_key
            })
                .then(info => {
                    if (info.code !== 200) {
                        return Promise.reject(info);
                    }

                    return AuthHelper.syncUserSession(info.data.uid, req, res);
                })
                .then(() => {
                    res.json({
                        code: 200,
                        message: LOGIN_SUCCSS,
                        redirect: req.cookies.refer
                    });

                    delete req.session.smsLogin;
                })
                .catch(error => {
                    res.json(error);
                });

        })
        .catch(next);
};

// AJAX 短信登录 设置密码 in step3
exports.password = (req, res, next) => {
    let step = _.get(req.session, 'smsLogin.step');

    if (step !== 3) {
        return next();
    }


    let data = {
        code: '400',
        message: BAD_PASSWORD
    };

    let mobile = _.get(req.session, 'smsLogin.mobile');
    let area = _.get(req.session, 'smsLogin.area');
    let password = (req.body.password || '').trim();
    let smsCode = +req.body.smsCode || 0;

    if (!password) {
        data.message = PASSWORD_REQUIRED;
        return res.json(data);
    }

    if (password.length < 6 || password.length > 20) {
        data.message = PASSWORD_LENGTH_ERROR;
        return res.json(data);
    }

    if (!/^(?![0-9]+$)(?![a-zA-Z]+$)[0-9A-Za-z]{6,20}$/.test(password)) {
        return res.json(data);
    }

    // 购物车key
    let shoppingKey = cookie.getShoppingKey(req);


    RegService.regMobileAes(area, mobile, password, shoppingKey, smsCode).then(result => {
        if (!result.code || result.code !== 200) {
            return Promise.reject(result);
        }
        if (!result.data || !result.data.uid) {
            return Promise.reject(result);
        }

        return AuthHelper.syncUserSession(result.data.uid, req, res);
    }).then(() => {
        res.json({
            code: 200,
            message: LOGIN_SUCCSS,
            redirect: req.cookies.refer || '/'
        });
        delete req.session.smsLogin;
    }).catch(next);
};


/**
 *  生成 校验码
 */
exports.genCaptcha = (req, res) => {
    let captcha = captchaService.generateCaptcha(90, 52, 4);

    _.set(req.session, 'smsLogin.captcha', captcha.text);

    res.type('png')
       .set('Cache-Control', 'no-cache')
       .status(200)
       .send(captcha.image);
};