Authored by 陈轩

Merge branch 'feature/phone-login' into release/4.9.2

/* eslint no-unused-vars: ["error", { "args": "none" }] */
/**
* 登录
* @author: Bi Kai<kai.bi@yoho.cn>
... ... @@ -93,6 +94,7 @@ const local = {
backUrl: 'javascript:history.go(-1)', // eslint-disable-line
showHeaderImg: true, // 控制显示头部图片
isPassportPage: true, // 模板中模块标识
smsLoginUrl: '/passport/sms_login',
registerUrl: '/passport/reg/index', // 注册的URL链接
aliLoginUrl: '/passport/login/alipay', // 支付宝快捷登录的URL链接
weiboLoginUrl: '/passport/login/sina', // 微博登录的URL链接
... ...
/* eslint no-unused-vars: ["error", { "args": "none" }] */
'use strict';
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');
// constrant
const CODE_REQUIRED = '请输入校验码';
const PASSWORD_REQUIRED = '请输入密码';
const BAD_PASSWORD = '密码格式不正确';
const TOO_MANY = '请求太频繁';
const LOGIN_SUCCSS = '登录成功';
const VERIFY_ERROR = '校验失败';
exports.beforeIn = (req, res, next) => {
if (!req.xhr && req.user.uid) {
return res.redirect(req.cookies.refer);
}
next();
};
// 短信登录 第一步: 展现页面
const _step1 = (req, res, next) => {
let template = 'sms/login';
let viewData = {
module: 'passport',
page: 'sms-login',
isPassportPage: true,
headerText: '手机号码快捷登录',
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',
backUrl: '/passport/sms_login?step=1',
isPassportPage: true,
headerText: '手机号码快捷登录',
canResend: interval < Date.now(),
mobile,
area
};
res.render(template, viewData);
};
// 短信登录 第三步: 设置密码 (针对 改手机未注册用户)
const _step3 = (req, res, next) => {
const template = 'sms/password';
const viewData = {
module: 'passport',
page: 'sms-password',
backUrl: '/passport/sms_login?step=2',
isPassportPage: true,
headerText: '设置密码'
};
res.render(template, viewData);
};
// 短信 登录
exports.loginPage = (req, res, next) => {
let step = Number(req.query.step) || 1;
let smsLoginStep = req.session.smsLoginStep || 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();
if (!req.xhr) {
return next(404);
}
if (req.session.smsLogin && req.session.smsLogin.interval > Date.now()) {
return res.json({
code: 429,
message: TOO_MANY
});
}
if ([area, mobile].some(val => val === '')) {
return res.json({
code: 401,
message: '请求参数,无法处理'
});
}
next();
};
// AJAX 获取验证码
exports.token = (req, res, next) => {
let mobile = req.query.area;
let area = req.query.mobile;
PhoneService.sendSMS(mobile, area, 1).then(result => {
if (result.code === 200) {
req.session.smsLogin = {
interval: Date.now() + 60 * 1000, // 重发验证码 间隔: 60s
area,
mobile
};
req.session.smsLoginStep = 2; // 进入短信登录 step2
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();
if (!req.xhr && req.session.smsLoginStep !== 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';
req.session.smsLoginStep = 3;
res.json({
code: 200,
redirect
});
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;
delete req.session.smsLoginStep;
})
.catch(error => {
res.json(error);
});
})
.catch(next);
};
// AJAX 短信登录 设置密码 in step3
exports.password = (req, res, next) => {
if (req.session.smsLoginStep !== 3) {
return next();
}
let data = {
code: '400',
message: BAD_PASSWORD
};
let smsLogin = req.session.smsLogin || {};
let mobile = smsLogin.mobile;
let area = smsLogin.area;
let password = (req.body.password || '').trim();
if (!password) {
data.message = PASSWORD_REQUIRED;
return res.json(data);
}
if (!helpers.verifyPassword(password)) {
return res.json(data);
}
// 购物车key
let shoppingKey = cookie.getShoppingKey(req);
RegService.regMobileAes(area, mobile, password, shoppingKey).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;
delete req.session.smsLoginStep;
}).catch(next);
};
... ...
/* eslint no-unused-vars: ["error", { "args": "none" }] */
'use strict';
const API = global.yoho.API;
class PhoneService {
// 校验 手机 是否 已注册
// http://git.yoho.cn/yoho-documents/api-interfaces/blob/master/个人中心/验证码登录/校验是否是注册用户.md
static checkUserPhoneExist(mobile, area) {
return API.get('', {
method: 'app.passport.checkUserExist',
mobile,
area
});
}
// 手机号 自动登录
// http://git.yoho.cn/yoho-documents/api-interfaces/blob/master/个人中心/验证码登录/手机号自动登录.md
static autoSignin(param) {
return API.get('', {
method: 'app.passport.autoSignin',
profile: param.profile,
area: param.area,
code: param.code,
shopping_key: param.shopping_key
});
}
// 发送 验证码
// http://git.yoho.cn/yoho-documents/api-interfaces/blob/master/个人中心/验证码登录/发送验证码.md
static sendSMS(mobile, area, type) {
if (process.env.NODE_ENV === 'development') {
return new Promise((resolve, reject) => {
return resolve({
alg: 'SALT_MD5',
code: 200,
data: {},
md5: '6d729d4b35f10fc73531210bd7ecff91',
message: '发送成功.'
});
});
}
return API.get('', {
method: 'app.message.sendSms',
mobile,
area,
type
});
}
// 校验 验证码
// http://git.yoho.cn/yoho-documents/api-interfaces/blob/master/个人中心/验证码登录/验证验证码.md
static verifySMS(mobile, area, code, type) {
if (process.env.NODE_ENV === 'development') {
return new Promise((resolve, reject) => {
return resolve({
alg: 'SALT_MD5',
code: 200,
data: {
is_pass: 'Y'
},
md5: '6d729d4b35f10fc73531210bd7ecff91',
message: '发送成功.'
});
});
}
return API.get('', {
method: 'app.message.verifySmsCode',
mobile,
area,
code,
type
});
}
}
module.exports = PhoneService;
... ...
... ... @@ -12,6 +12,7 @@ const login = require(cRoot + '/login');
const back = require(cRoot + '/back');
const bind = require(cRoot + '/bind');
const reg = require(cRoot + '/reg');
const smsLogin = require(cRoot + '/sms');
const router = express.Router(); // eslint-disable-line
... ... @@ -33,6 +34,17 @@ router.get('/passport/international', login.common.beforeLogin, login.local.inte
// 本地登录
router.post('/passport/login/auth', login.local.login);
// SMS 短信
router.use('/passport/sms_login', login.common.beforeLogin, smsLogin.beforeIn);
router.get('/passport/sms_login', smsLogin.loginPage);
router.get('/passport/sms_login/token.json',
smsLogin.tokenBefore,
smsLogin.token); // only ajax;
router.get('/passport/sms_login/check.json',
smsLogin.checkBefore,
smsLogin.check); // only ajax
router.post('/passport/sms_login/password.json', smsLogin.password);
// 微信登录
router.get('/passport/login/wechat', login.common.beforeLogin, login.wechat.login);
router.get('/passport/login/wechat/callback', login.wechat.callback);
... ...
... ... @@ -10,7 +10,7 @@
</div>
<span id="btn-login" class="btn btn-login disable">登录</span>
<p class="op-container">
<a class="go-register" href={{registerUrl}}>免费注册</a>
<a class="sms-login" href={{smsLoginUrl}}>手机号码快捷登录</a>
<span id="forget-pwd" class="forget-pwd">忘记密码</span>
</p>
<div class="third-party-login">
... ... @@ -22,6 +22,10 @@
</div>
</div>
<a class="international" href={{internationalUrl}}>International Customer</a>
<div class="go-register">
<i class="iconfont">&#xe610;</i>
<a href={{registerUrl}}>注册Yoho!Family</a>
</div>
<div class="login-tip">
<div class="info-icon"></div>
Yoho!Family账号可登录Yoho!Buy有货
... ...
<div class="sms-login passport-page yoho-page">
{{> passport/header}}
<div class="content">
<p class="sms-login-msg">验证码已发至&nbsp;<span class="tel">+86 133601454888</span></p>
<div class="input-container input-group sms-input row">
<input id="sms-code" class="input" type="text" placeholder="验证码">
<span class="input-addon">
<button type="button" id="resend-sms">重发验证码</button>
</span>
</div>
<button id="btn-next" class="btn btn-next disable row" type="button">登录</button>
</div>
<input type="hidden" name="mobile" id="mobile" value="{{mobile}}">
<input type="hidden" name="area" id="area" value="{{area}}">
</div>
<script>
var canResend = {{canResend}};
</script>
\ No newline at end of file
... ...
<div class="sms-login-page passport-page yoho-page">
{{> passport/header}}
<div class="content">
{{> passport/country_list}}
<div class="input-container phone-container row has-clear">
<span id="area-code" class="area-code">{{areaCode}}</span>
<input id="phone-num" class="input phone-num" type="text" placeholder="手机号">
</div>
<span id="btn-next" class="btn btn-next disable row">获取短信验证码</span>
</div>
</div>
\ No newline at end of file
... ...
<div class="sms-login passport-page yoho-page">
{{> passport/header}}
<div class="content">
<p class="sms-login-msg small">你以后还可以使用手机号码 + 密码的形式登录有货哦!</p>
<div class="input-container row has-eye">
<input id="pwd" class="pwd input" type="password" placeholder="密码">
<div class="eye close" id="eye"></div>
</div>
<span id="btn-next" class="btn btn-next disable row">确定</span>
</div>
</div>
\ No newline at end of file
... ...
... ... @@ -16,9 +16,16 @@ module.exports = {
domains: {
// api: 'http://devapi.yoho.cn:58078/',
// service: 'http://devservice.yoho.cn:58077/'
api: 'http://api-test3.yohops.com:9999/',
service: 'http://service-test3.yohops.com:9999/',
liveApi: 'http://testapi.live.yohops.com:9999/'
// api: 'http://api-test3.yohops.com:9999/',
// service: 'http://service-test3.yohops.com:9999/',
// liveApi: 'http://testapi.live.yohops.com:9999/'
api: 'http://api.yoho.cn/',
service: 'http://service.yoho.cn/'
// api: 'http://testapi.yoho.cn:28078/',
// service: 'http://testservice.yoho.cn:28077/'
},
subDomains: {
host: '.m.yohobuy.com',
... ...
var tip = require('plugin/tip');
var $resendBtn,
$nextBtn,
$smsCode,
mobile, area;
var page = {
disableAjax: false,
time: 60,
resendText: '重发验证码',
timerId: null,
init: function() {
this.domInit();
this.bindEvents();
if (!window.canResend) {
this.countDown();
}
},
domInit: function() {
$resendBtn = $('#resend-sms');
$nextBtn = $('#btn-next');
$smsCode = $('#sms-code');
mobile = $('#mobile').val();
area = $('#area').val();
},
bindEvents: function() {
var self = this;
$resendBtn.on('click', function() {
self.resendSMS();
});
$smsCode.on('input', function() {
var hasVal = Boolean($.trim(this.value));
$nextBtn.toggleClass('disable', !hasVal);
});
$nextBtn.on('click', function() {
!self.disableAjax && self.submit();
});
},
countDown: function() {
var self = this;
var second = this.time;
if (this.timerId) {
return;
}
$resendBtn.prop('disable', true);
this.timerId = setInterval(function() {
var txt = self.resendText;
second = second - 1;
if (second < 0) {
clearInterval(self.timerId);
self.timerId = null;
$resendBtn.prop('disable', false);
} else {
txt = second + 's';
}
$resendBtn.text(txt);
}, 1000);
},
resendSMS: function() {
var self = this;
if ($resendBtn.prop('disable')) {
return;
}
$.get('/passport/sms_login/token.json', {
area: area,
mobile: mobile,
})
.done(function(res) {
if (res.code === 200) {
self.countDown();
return;
}
tip.show(res.message);
})
.fail(function() {
tip.show('出错啦~休息一下');
});
},
submit: function() {
var self = this;
var code = $.trim($smsCode.val());
this.disableAjax = true;
$.get('/passport/sms_login/check.json', {
code: code
})
.done(function(res) {
if (res.code === 200) {
location.href = res.redirect;
return;
}
tip.show(res.message);
})
.fail(function() {
tip.show('出错了, 请重试');
})
.always(function() {
self.disableAjax = false;
});
}
};
$(function() {
page.init();
});
... ...
'use strict';
var tip = require('plugin/tip');
var api = require('./api');
var $countrySelect,
$areaCode,
$nextBtn,
$phoneNum;
var page = {
disableAjax: false,
init: function() {
this.domInit();
this.bindEvent();
this.toggleNextBtn();
},
domInit: function() {
$countrySelect = $('#country-select');
$areaCode = $('#area-code');
$nextBtn = $('#btn-next');
$phoneNum = $('#phone-num');
},
bindEvent: function() {
var self = this;
$countrySelect.on('change', function() {
$areaCode.text(this.value);
});
$phoneNum.on('input', function() {
self.toggleNextBtn();
});
$nextBtn.on('click', function() {
!self.disableAjax && self.goNext();
});
},
// 切换$nextBtn disable状态
toggleNextBtn: function() {
var bool = Boolean($.trim($phoneNum.val()));
$nextBtn
.toggleClass('disable', !bool)
.prop('disable', !bool);
},
// 提交按钮
goNext: function() {
var areaCode = $countrySelect.val();
var phone = $.trim($phoneNum.val());
var self = this;
if ($nextBtn.hasClass('disable')) {
return;
}
if (!api.phoneRegx[areaCode].test(phone)) {
tip.show('手机号码格式不正确, 请重新输入');
return;
}
this.disableAjax = true;
$.get('/passport/sms_login/token.json', {
area: areaCode.replace('+', ''),
mobile: phone
})
.done(function(data) {
if (data.code === 200) {
location.href = data.redirect;
} else {
tip.show(data.message);
}
})
.fail(function() {
tip.show('出错了, 请重试');
})
.always(function() {
self.disableAjax = false;
});
}
};
$(function() {
page.init();
});
... ...
var tip = require('plugin/tip');
var $eyeBtn,
$pwd,
$nextBtn;
var page = {
disableAjax: false,
init: function() {
this.domInit();
this.bindEvent();
},
domInit: function() {
$eyeBtn = $('#eye');
$pwd = $('#pwd');
$nextBtn = $('#btn-next');
},
bindEvent: function() {
var self = this;
$eyeBtn.on('click', function() {
self.togglePassword();
});
$nextBtn.on('click', function() {
!self.disableAjax && self.setPasswordAndLogin();
});
$pwd.on('input', function() {
var bool = Boolean($.trim(this.value));
$nextBtn
.toggleClass('disable', !bool)
.prop('disable', !bool);
});
},
togglePassword: function() {
var bool = $eyeBtn.hasClass('close');
$eyeBtn.toggleClass(function() {
$pwd.attr('type', !bool ? 'password' : 'text');
return 'close';
}, !bool);
},
setPasswordAndLogin: function() {
var self = this;
var password = $.trim($pwd.val());
this.disableAjax = true;
$.post('/passport/sms_login/password.json', {
password: password
})
.done(function(res) {
if (res.code === 200) {
location.href = res.redirect;
return;
}
tip.show(res.message);
})
.fail(function() {
tip.show('出错了, 请重试');
})
.always(function() {
self.disableAjax = false;
});
}
};
$(function() {
page.init();
});
... ...
@charset "utf-8";
@import "me/index";
@import "layout/reset";
@import "layout/common";
... ... @@ -7,6 +8,7 @@
@import "layout/header";
@import "layout/footer";
@import "layout/utils";
@import "layout/form";
@import "common/index";
@import "channel/index";
@import "product/index";
... ...
.input-group {
display: table;
.input {
width: 100%;
}
.input,
.input-addon {
display: table-cell;
vertical-align: middle;
}
}
... ...
@import "common";
@import "register";
@import "login";
@import "sms-login";
@import "back";
@import "code";
@import "bind";
... ...
... ... @@ -3,7 +3,7 @@
position: absolute;
height: 31PX;
width: 26PX;
background: resolve('passport/yoho.png');
background: resolve("passport/yoho.png");
background-size: 100% 100%;
top: 10PX;
left: 15PX;
... ... @@ -20,7 +20,7 @@
text-align: left;
font-size: 16PX;
.go-register {
.sms-login {
text-decoration: underline;
color: #858585;
}
... ... @@ -57,20 +57,20 @@
}
.alipay {
background-image: resolve('passport/alipay.png');
background-image: resolve("passport/alipay.png");
}
.weibo {
background-image: resolve('passport/weibo.png');
background-image: resolve("passport/weibo.png");
}
.qq {
background-image: resolve('passport/qq.png');
background-image: resolve("passport/qq.png");
}
.wechat {
display: none;
background-image: resolve('passport/wechat.png');
background-image: resolve("passport/wechat.png");
}
}
}
... ... @@ -82,11 +82,19 @@
background-color: #333;
border: none;
border-radius: 20PX;
margin: 0 auto;
margin: 0 auto 28px;
font-size: 16PX;
color: #d8d8d8;
}
.go-register {
color: #858585;
a {
color: inherit;
}
}
.login-tip {
font-size: 16PX;
position: relative;
... ... @@ -97,7 +105,7 @@
display: inline-block;
height: 12PX;
width: 12PX;
background-image: resolve('passport/info.png');
background-image: resolve("passport/info.png");
background-size: 100% 100%;
}
}
... ...
.sms-login {
.sms-input {
margin-top: 60px;
}
#resend-sms {
display: block;
background-color: transparent;
width: 190px;
margin-right: 15px;
border: 1px solid #36a74c;
border-radius: 26px;
font-size: 20px;
line-height: 30px;
color: #36a74c;
}
button {
border: none;
}
}
.sms-login-msg {
font-size: 28px;
color: #fff;
margin-bottom: 20px;
.tel {
color: #41cbe7;
}
&.small {
font-size: 22px;
color: #858585;
}
}
... ...
... ... @@ -42,7 +42,7 @@ module.exports = {
resolve: {
alias: {
common: path.join(__dirname, 'js/common'),
plugin: path.join(__dirname, 'js/plugins')
plugin: path.join(__dirname, 'js/plugin')
}
},
plugins: [
... ...