import events from 'events' //rollup編譯時得剔除events
import cloneDeep from 'lodash-es/cloneDeep.js'
import each from 'lodash-es/each.js'
import map from 'lodash-es/map.js'
import get from 'lodash-es/get.js'
import concat from 'lodash-es/concat.js'
import join from 'lodash-es/join.js'
//import wrap from 'lodash-es/wrap.js'
import pick from 'lodash-es/pick.js'
import dayjs from 'dayjs' //rollup編譯時得剔除dayjs
import genPm from 'wsemi/src/genPm.mjs'
import genID from 'wsemi/src/genID.mjs'
import replaceObj from 'wsemi/src/replaceObj.mjs'
import haskey from 'wsemi/src/haskey.mjs'
import now2str from 'wsemi/src/now2str.mjs'
import binstr from 'wsemi/src/binstr.mjs'
import isstr from 'wsemi/src/isstr.mjs'
import isestr from 'wsemi/src/isestr.mjs'
import isobj from 'wsemi/src/isobj.mjs'
import iseobj from 'wsemi/src/iseobj.mjs'
import isarr from 'wsemi/src/isarr.mjs'
import ispint from 'wsemi/src/ispint.mjs'
import isfun from 'wsemi/src/isfun.mjs'
import istimeTZ from 'wsemi/src/istimeTZ.mjs'
import isEmail from 'wsemi/src/isEmail.mjs'
import str2sha512 from 'wsemi/src/str2sha512.mjs'
import pm2resolve from 'wsemi/src/pm2resolve.mjs'
import WComorHapiServer from 'w-comor-hapi/src/WComorHapiServer.mjs' //rollup編譯時得剔除@hapi/hapi
//import WOrm from 'w-orm-mongodb/src/WOrmMongodb.mjs' //rollup編譯時得剔除mongodb與stream
import WEmail from 'w-email/src/WEmail.mjs' //rollup編譯時得剔除nodemailer
import emLetterHtml from './emLetterHtml.mjs'
import emSignUpHtml from './emSignUpHtml.mjs'
import emResetPWHtml from './emResetPWHtml.mjs'
import verifyWebHtml from './verifyWebHtml.mjs'
import verifyWebLogo from './verifyWebLogo.mjs'
function toStrDate(d) {
return d.format('YYYY-MM-DDTHH:mm:ssZ')
}
function fromStrDate(c) {
return dayjs(c, 'YYYY-MM-DDTHH:mm:ssZ')
}
function getSuccess(msg = null) {
return {
state: 'success',
msg,
}
}
function getError(msg = null) {
return {
state: 'error',
msg,
}
}
//defRole
let defRole = {
roleSystem: 'system',
roleGeneral: 'general',
}
//defActive
let defActive = {
activeYes: 'yes',
activeNo: 'no',
}
//defExclude
let defExclude = ['<', '>']
/**
* @class WUserServer::Private
*/
/**
* 建立使用者hapi伺服器
*
* @class
* @param {Object} [opt={}] 輸入設定物件,預設{}
* @param {Object} [opt.orm={}] 輸入操作資料庫物件,預設{}
* @param {Object} [opt.serverHapi={}] 輸入hapi伺服器物件,若提供,本服務將自動加入api至route。使用外部hapi伺服器時,需開啟跨域功能,或是使用nginx反向代理轉入api請求
* @param {Integer} [opt.port=8080] 輸入Hapi伺服器所在port整數,預設8080
* @param {String} [opt.apiName='api'] 輸入Hapi伺服器api接口名稱字串,預設'api'
* @param {String} [opt.authUrl='http://localhost:8080/auth'] 輸入Hapi伺服器外部連入驗證網址字串,末端api路徑需為auth,預設'http://localhost:8080/auth'
* @param {String} [opt.salt=''] 輸入密碼加密用鹽字串,預設''
* @param {Integer} [opt.timeTokenExp=30] 輸入token過期時間整數,預設30(分鐘)
* @param {String} [opt.webName=''] 輸入email模板內網站名稱字串,預設''
* @param {String} [opt.webUrl=''] 輸入email模板內網站外部網址首頁字串,預設''
* @param {String} [opt.webDescription=''] 輸入email模板內網站簡易描述字串,預設''
* @param {String} [opt.emSenderName='WUserServer'] 輸入寄件人顯示名稱字串,預設'WUserServer'
* @param {String} opt.emSenderEmail 輸入寄件人email信箱字串
* @param {String} [opt.emSenderPW=''] 輸入寄件人email密碼字串,預設''
* @param {String} [opt.emBCC=''] 輸入寄信備份用之密件收件人email字串,預設''
* @param {String} [opt.emLetterHtml='file:emLetterHtml.mjs'] 輸入email模板字串,大括號標注字串為系統取代字串,預設模板文字放於'emLetterHtml.mjs'
* @param {String} [opt.emLetterTeamMessage='Global-User International Company'] 輸入email模板內標注團隊字串,預設'Global-User International Company'
* @param {String} [opt.emLetterDoNotReplayMessage='This letter is automatically sent by the system, please do not reply to this email.'] 輸入email模板內請勿回覆字串,預設'This letter is automatically sent by the system, please do not reply to this email.'
* @param {Array} [opt.emLetterLinks=[]] 輸入email模板內超連結資訊物件,超連結物件需給予name與url兩屬性,預設[]
* @param {String} [opt.emSignUpTitle='Verification letter'] 輸入註冊驗證email標題字串,預設'Verification letter'
* @param {String} [opt.emSignUpHtml='file:emSignUpHtml.mjs'] 輸入註冊驗證email html模板字串,大括號標注字串為系統取代字串,預設模版文字放於'emSignUpHtml.mjs'
* @param {String} [opt.emResetPWTitle='Password reset letter'] 輸入重設密碼驗證email標題字串,預設'Password reset letter'
* @param {String} [opt.emResetPWHtml='file:emResetPWHtml.mjs'] 輸入重設密碼驗證email html模板字串,大括號標注字串為系統取代字串,預設模版文字放於'emResetPWHtml.mjs'
* @param {String} [opt.verifyWebHtml='file:verifyWebHtml.mjs'] 輸入驗證連結網站html模板字串,大括號標注字串為系統取代字串,預設模版文字放於'verifyWebHtml.mjs'
* @param {String} [opt.verifyWebLogo='file:verifyWebLogo.mjs'] 輸入驗證連結網站用logo圖字串,預設圖字串放於'verifyWebLogo.mjs',為svg圖字串
* @param {String} [opt.verifyWebMsgSuccess='Verify successfully'] 輸入驗證連結網站當成功驗證時之顯示字串,預設'Verify successfully'
* @param {String} [opt.verifyWebMsgExpired='Code has expired'] 輸入驗證連結網站當無法驗證時之顯示字串,預設'Code has expired'
* @param {String} [opt.verifyWebMsgCount='Go to the website automatically after {s} seconds...'] 輸入驗證連結網站當成功驗證時之倒數文字字串,預設'Go to the website automatically after {s} seconds..Promise.'
* @param {Object} [opt.funcs={}] 輸入擴充指定執行函數物件,預設{}
* @param {Array} [opt.routes=[]] 輸入擴充hapi routes陣列,預設[]
* @returns {Object} 回傳通訊物件
* @example
* import WUserServer from 'WUserServer/src/w-user-server.mjs'
*
*
* //webName
* let webName = 'User management system'
* // let webName = '使用者管理系統'
*
*
* //webUrl
* let webUrl = 'https://google.com'
*
*
* //webDescription
* let webDescription = "Let's do something and make it easier"
* // let webDescription = '讓我們一起做更簡單的事'
*
*
* //orm
* let orm = WOrmMongodb({
* url: 'mongodb://username:password@127.0.0.1:27017',
* db: 'wuser',
* cl: 'users',
* })
*
*
* //opt
* let opt = {
* orm,
* port: 8080,
* apiName: 'api',
* authUrl: 'http://localhost:8080/auth', //need webUrl+'/'+auth, use 'http://localhost:8080' in test case
* salt: 'zLAUfSLDUuausd0Aasu912SDU', //generate it for sites
* timeTokenExp: 30,
*
* emSenderName: webName,
* emSenderEmail: "sender's email", //email address for email sender
* emSenderPW: "sender's password", //password for email sender
*
* webName,
* webUrl,
* webDescription,
*
* // emLetterTeamMessage: `${webName}開發團隊 敬上`,
* // emLetterDoNotReplayMessage: '本信由系統自動發信,請勿回信',
* emLetterLinks: [{ name: 'Google', url: 'https://google.com' }],
*
* // emSignUpTitle: '註冊驗證信',
* // emSignUpHtml: `<p>親愛的 {name} 您好:</p><p>已收到您的註冊申請,請點擊下方連結進行驗證,確定驗證成功後即可登入。</p><p><a href="{urlCheckCode}" target="_blank">點擊此處驗證</a></p>`,
*
* // emResetPWTitle: '重設密碼驗證信',
* // emResetPWHtml: `<p>親愛的 {name} 您好:</p><p>已收到您的重設密碼申請,請點擊下方連結進行驗證,確定驗證成功後即可登入。</p><p><a href="{urlCheckCode}" target="_blank">點擊此處驗證</a></p>`,
*
* // verifyWebMsgSuccess: '驗證成功',
* // verifyWebMsgExpired: '驗證失敗',
* // verifyWebMsgCount: '{s} 秒後自動前往網站',
*
* funcs: {},
* routes: [],
* }
*
*
* //new
* let wus = new WUserServer(opt)
* wus.on('all', function({ eventName, data }) {
* console.log('all:' + eventName, ...data)
* })
*/
function WUserServer(opt = {}) {
//ee
let ee = new events.EventEmitter()
//eeEmit
function eeEmit(name, ...args) {
setTimeout(() => {
ee.emit(name, ...args)
ee.emit('all', { eventName: name, data: [...args] })
}, 1)
}
//pmWithEmit
function pmWithEmit(fn, eventName = null) {
return function() {
let pm = genPm()
//args
let args = cloneDeep(arguments) //因物件記憶體同區, 進函式之後被更改皆會有影響, 等then或catch結束, 原本函數的輸入數據會變成已變更數據
function core(msg) {
if (eventName) {
eeEmit(eventName, {
input: args,
output: msg,
})
}
}
fn.apply(this, arguments)
.then(function(msg) {
core(msg)
pm.resolve(msg)
})
.catch(function(err) {
core(err)
pm.resolve(err)
})
return pm
}
}
// function pmWithEmit(f, eventName = null) {
// return wrap(f, function(func, ...args) {
// let pm = genPm()
// function core(msg) {
// if (eventName) {
// eeEmit(eventName, {
// input: [...args],
// output: msg,
// })
// }
// }
// func(...args)
// .then(function(msg) {
// core(msg)
// pm.resolve(msg)
// })
// .catch(function(err) {
// core(err)
// pm.resolve(err)
// })
// return pm
// })
// }
//cloneDeep
opt = cloneDeep(opt)
//default
if (!ispint(opt.port)) {
opt.port = 8080
}
if (!isestr(opt.apiName)) {
opt.apiName = 'api'
}
if (!isestr(opt.authUrl)) {
opt.authUrl = 'http://localhost:8080/auth'
}
if (!isestr(opt.salt)) {
opt.salt = '' //salt for password
}
if (!ispint(opt.timeTokenExp)) {
opt.timeTokenExp = 30
}
if (!isestr(opt.webName)) {
opt.webName = ''
}
if (!isestr(opt.webUrl)) {
opt.webUrl = ''
}
if (!isestr(opt.webDescription)) {
opt.webDescription = ''
}
if (!isestr(opt.emSenderName)) {
opt.emSenderName = 'WUserServer'
}
if (!isestr(opt.emSenderEmail)) {
console.log('invalid emSenderEmail')
return ee
}
if (!isestr(opt.emSenderPW)) {
opt.emSenderPW = ''
}
if (!isestr(opt.emBCC)) {
opt.emBCC = ''
}
if (!isestr(opt.emLetterHtml)) {
opt.emLetterHtml = emLetterHtml
}
if (!isestr(opt.emLetterTeamMessage)) {
opt.emLetterTeamMessage = 'Global-User International Company'
}
if (!isestr(opt.emLetterDoNotReplayMessage)) {
opt.emLetterDoNotReplayMessage = 'This letter is automatically sent by the system, please do not reply to this email.'
}
if (!isarr(opt.emLetterLinks)) {
opt.emLetterLinks = []
}
if (!isestr(opt.emSignUpTitle)) {
opt.emSignUpTitle = 'Verification letter'
}
if (!isestr(opt.emSignUpHtml)) {
opt.emSignUpHtml = emSignUpHtml
}
if (!isestr(opt.emResetPWTitle)) {
opt.emResetPWTitle = 'Password reset letter'
}
if (!isestr(opt.emResetPWHtml)) {
opt.emResetPWHtml = emResetPWHtml
}
if (!isestr(opt.verifyWebHtml)) {
opt.verifyWebHtml = verifyWebHtml
}
if (!isestr(opt.verifyWebLogo)) {
opt.verifyWebLogo = verifyWebLogo
}
if (!isestr(opt.verifyWebMsgSuccess)) {
opt.verifyWebMsgSuccess = 'Verify successfully'
}
if (!isestr(opt.verifyWebMsgExpired)) {
opt.verifyWebMsgExpired = 'Code has expired'
}
if (!isestr(opt.verifyWebMsgCount)) {
opt.verifyWebMsgCount = 'Go to the website automatically after {s} seconds...'
}
if (!isobj(opt.funcs)) {
opt.funcs = {}
}
if (!isarr(opt.routes)) {
opt.routes = []
}
let bfSelect = isfun(get(opt, 'orm.select', null))
let bfInsert = isfun(get(opt, 'orm.insert', null))
let bfSave = isfun(get(opt, 'orm.save', null))
if (!bfSelect || !bfInsert || !bfSave) {
console.log('invalid orm')
return ee
}
//worm, need select insert save
let worm = opt.orm
//schema
let sc = {
id: {
type: 'String',
public: true,
necessary: true,
exclude: [],
canModify: false,
},
name: {
type: 'String',
public: true,
necessary: false,
exclude: defExclude,
canModify: true,
},
pwEnc: {
type: 'String',
public: false,
necessary: true,
exclude: [],
canModify: false,
},
email: {
type: 'String',
public: true,
necessary: true,
exclude: defExclude,
canModify: true,
},
address: {
type: 'String',
public: true,
necessary: false,
exclude: defExclude,
canModify: true,
},
phone: {
type: 'String',
public: true,
necessary: false,
exclude: defExclude,
canModify: true,
},
organization: {
type: 'String',
public: true,
necessary: false,
exclude: defExclude,
canModify: true,
},
position: {
type: 'String',
public: true,
necessary: false,
exclude: defExclude,
canModify: true,
},
role: {
type: 'String', //roleGeneral|roleSystem
public: true,
necessary: true,
exclude: defExclude,
canModify: false,
},
checkCode: {
type: 'String',
public: false,
necessary: false,
exclude: [],
canModify: false,
},
token: {
type: 'String',
public: false,
necessary: false,
exclude: [],
canModify: false,
},
tokenExp: {
type: 'String',
public: false,
necessary: false,
exclude: [],
canModify: false,
},
timeCreate: {
type: 'String',
public: false,
necessary: true,
exclude: [],
canModify: false,
},
timeLogin: {
type: 'String',
public: false,
necessary: false,
exclude: [],
canModify: false,
},
remark: {
type: 'String', //'Object',
public: true,
necessary: false,
exclude: [],
canModify: true,
},
active: {
type: 'String', //activeYes|activeNo
public: false,
necessary: true,
exclude: [],
canModify: false,
},
}
let wsaveR = pm2resolve(worm.save)
function verifyPropCore(k, v) {
if (haskey(sc, k)) {
let t = sc[k].type
let e = sc[k].exclude
if (t === 'String') {
if (!isstr(v)) {
return getError('value is not string: ' + v)
}
if (binstr(v, e)) {
return getError('can not contain special characters: "<" or ">"')
}
return getSuccess()
}
else if (t === 'Object') {
if (!isobj(v)) {
return getError('value is not object: ' + v)
}
return getSuccess()
}
return getError('type is not defined: ' + t)
}
else {
return getError('invalid key: ' + k)
}
}
/**
* 驗證使用者資料欄位
*
* @param {Object} u 輸入使用者資料物件
* @returns {Object} 回傳判斷物件,state為'success'為成功驗證,state為'error'為有錯誤,msg為儲存錯誤訊息
*/
function verifyProp(u) {
each(u, function(v, k) {
let r = verifyPropCore(k, v)
if (r.state === 'error') {
return r
}
})
return getSuccess()
}
/**
* 針對使用者資料物件處理,若無給指定欄位,則自動給予預設值
*
* @param {Object} u 輸入使用者資料物件
* @returns {Object} 回傳使用者資料物件,指定欄位若沒給則自動填入預設值
*/
function defaultProp(u) {
each(sc, function(v, k) {
let def = ''
if (v.type === 'Object') {
def = {}
}
if (!haskey(u, k)) {
u[k] = def
}
else if (u[k] === null || u[k] === undefined) {
u[k] = def
}
})
return u
}
async function selectPublic({ token, find }) {
let pm = genPm()
let r = null
//isValidTokenR
r = await isValidTokenR(token)
//cehck
if (r.state === 'error') {
pm.reject(r.msg)
return pm
}
//select
worm.select(find)
.then(function(msg) {
if (msg.length >= 1) {
//uesrs
let users = msg
//cols, 允許公開的欄位
let cols = []
each(sc, function(v, k) {
if (v.public) {
cols.push(k)
}
})
//user, 重新提取允許公開的欄位
users = map(users, function(u) {
return pick(u, cols)
})
pm.resolve(users) //公開查詢用, 只能提供公開欄位資訊
}
else {
pm.reject('can not find user')
}
})
.catch(function(err) {
console.log('selectPublic catch:', err, 'find:', find)
pm.reject('error')
})
return pm
}
let selectPublicR = pm2resolve(selectPublic)
/**
* 新增使用者
*
* @memberof WUserServer::Private
* @function insert
* @param {Object} user 輸入使用者物件
* @returns {Promise} 回傳Promise,resolve回傳使用者id與驗證碼(checkCode),reject代表錯誤並回傳錯誤訊息
*/
async function insert(user) {
let pm = genPm()
let r = null
//check, 需有加密密碼
if (!isestr(user.pwEnc)) {
pm.reject('invalid password')
return pm
}
//check, 需有email
if (!isEmail(user.email)) {
pm.reject('invalid email')
return pm
}
//check email
let existEmail = await isExist({ email: user.email })
if (existEmail) {
pm.reject('email has been used')
return pm
}
//verifyProp
r = verifyProp(user)
if (r.state === 'error') {
pm.reject(r.msg)
return pm
}
//defaultProp
user = defaultProp(user)
//id
user.id = genID()
//加密密碼加鹽再加密
if (opt.salt !== '') {
user.pwEnc = str2sha512(user.pwEnc + opt.salt)
}
//checkCode
user.checkCode = genID()
//timeCreate
user.timeCreate = now2str()
//role
user.role = defRole.roleGeneral
//active
user.active = defActive.activeYes
//insert
worm.insert(user)
.then(function(msg) {
pm.resolve({
id: user.id,
checkCode: user.checkCode,
})
})
.catch(function(err) {
console.log('insert catch:', err, 'user:', user)
pm.reject('error')
})
return pm
}
let insertR = pm2resolve(insert)
/**
* 查詢單一使用者
*
* @memberof WUserServer::Private
* @function selectOne
* @param {Object} find 輸入查詢物件
* @returns {Promise} 回傳Promise,resolve回傳單一使用者資訊物件,reject代表錯誤並回傳錯誤訊息
*/
function selectOne(find) {
let pm = genPm()
//select
worm.select(find)
.then(function(msg) {
if (msg.length === 1) {
pm.resolve(msg[0])
}
else if (msg.length > 1) {
pm.reject(`find ${msg.length} users`)
}
else {
pm.reject('can not find user')
}
})
.catch(function(err) {
console.log('selectOne catch:', err, 'find:', find)
pm.reject('error')
})
return pm
}
let selectOneR = pm2resolve(selectOne)
/**
* 判斷使用者是否存在
*
* @memberof WUserServer::Private
* @function isExist
* @param {Object} find 輸入查詢物件
* @returns {Promise} 回傳Promise,resolve回傳是否存在,reject代表錯誤並回傳錯誤訊息
*/
function isExist(find) {
let pm = genPm()
//check
if (!iseobj(find)) {
pm.reject('invalid find')
return pm
}
//select
worm.select(find)
.then(function(msg) {
if (msg.length >= 1) { //不管找到超過1位使用者是否合理, 有就代表存在
pm.resolve(true)
}
else {
pm.resolve(false)
}
})
.catch(function(err) {
console.log('select catch:', err, 'find:', find)
pm.reject('error')
})
return pm
}
async function signUp(user) {
let pm = genPm()
let r = null
//insertR
r = await insertR(user)
//cehck
if (r.state === 'error') {
pm.reject(r.msg) //已封裝過可直接回傳
return pm
}
//user
let id = r.msg.id
user.id = id
//checkCode
let checkCode = r.msg.checkCode
user.checkCode = checkCode
//urlCheckCode
let urlCheckCode = `${opt.authUrl}?checkCode=${checkCode}`
//sendEmailWhenSignUpR, 寄信給使用者驗證碼(checkCode), 點擊後才視為驗證成功
r = await sendEmailWhenSignUpR(user.name, urlCheckCode, user.email)
//cehck
if (r.state === 'error') {
console.log('signUp catch:', r.msg, 'emailObj:', { name: user.name, urlCheckCode, email: user.email })
pm.reject('can not send email')
return pm
}
pm.resolve(user.id)
return pm
}
let signUpR = pm2resolve(signUp)
async function logIn({ email, pwEnc }) {
let pm = genPm()
let r = null
//check
if (!isestr(email)) {
pm.reject('invalid email')
return pm
}
if (!isestr(pwEnc)) {
pm.reject('invalid password')
return pm
}
//find
let find = { email } //先查詢email是否存在
//selectOneR
r = await selectOneR(find)
//cehck
if (r.state === 'error') {
console.log('logIn catch:', r.msg, 'find:', find)
pm.reject('email or password is incorrect') //找不到使用者, 回傳訊息統一為email或密碼不正確
return pm
}
//user
let user = r.msg
//check checkCode, 帳號需驗證後才能登入, 於檢查密碼之前檢查, 避免使用者更換密碼未驗證前檢核密碼一定錯誤問題
if (user.checkCode !== '') {
pm.reject('account is not verified')
return pm
}
//加密密碼加鹽再加密
if (opt.salt !== '') {
pwEnc = str2sha512(pwEnc + opt.salt)
}
//check pwEnc, 確認密碼是否正確
if (user.pwEnc !== pwEnc) {
pm.reject('email or password is incorrect') //密碼錯誤, 回傳訊息統一為email或密碼不正確
return pm
}
//check active, 帳號需有效才能登入
if (user.active !== defActive.activeYes) {
pm.reject('account is suspended')
return pm
}
//isValidTokenR
r = await isValidTokenR(user.token)
//cehck
if (r.state === 'success') {
pm.resolve(user.token) //已登入則改回傳既有token
return pm
}
//token
let token = genID()
//userModify
let userModify = {
id: user.id,
token,
tokenExp: toStrDate(dayjs().add(opt.timeTokenExp, 'minute')),
timeLogin: now2str()
}
//wsaveR
r = await wsaveR(userModify)
//cehck
if (r.state === 'error') {
console.log('logIn catch:', r.msg, 'userModify:', userModify)
pm.reject('can not save token')
return pm
}
pm.resolve(token)
return pm
}
let logInR = pm2resolve(logIn)
async function logOut(token) {
let pm = genPm()
let r = null
//check
if (!isestr(token)) {
pm.reject('invalid token')
return pm
}
//find
let find = { token }
//selectOneR
r = await selectOneR(find)
//cehck
if (r.state === 'error') {
console.log('logOut catch:', r.msg, 'find:', find)
pm.reject('invalid token')
return pm
}
//user
let user = r.msg
//userModify
let userModify = {
id: user.id,
token: '',
tokenExp: ''
}
//wsaveR
r = await wsaveR(userModify)
//cehck
if (r.state === 'error') {
console.log('logOut catch:', r.msg, 'userModify:', userModify)
pm.reject('can not update token')
return pm
}
pm.resolve('ok')
return pm
}
let logOutR = pm2resolve(logOut)
/**
* 清除驗證瑪
*
* @memberof WUserServer::Private
* @function verifyCheckCode
* @param {String} checkCode
* @returns {Promise} 回傳Promise,resolve回傳使用者id,reject代表錯誤並回傳錯誤訊息
*/
function verifyCheckCode(checkCode) {
let pm = genPm()
//check
if (!isestr(checkCode)) {
pm.reject(opt.verifyWebMsgExpired)
return pm
}
//find
let find = { checkCode }
//selectOne
selectOne(find)
.then(function(user) {
//userModify
let userModify = {
id: user.id,
checkCode: ''
}
//save
return worm.save(userModify)
})
.then(function(msg) {
pm.resolve(opt.verifyWebMsgSuccess)
})
.catch(function(err) {
console.log('verifyCheckCode catch:', err, 'find:', find)
pm.reject(opt.verifyWebMsgExpired)
})
return pm
}
/**
* 產生驗證結果網站用html
*
* @memberof WUserServer::Private
* @function genVerifyHtml
* @param {String} [rdMessage=''] 輸入訊息字串
* @param {String} [rdCountdown='false'] 輸入是否顯示倒數文字字串,可選為'true'或'false'
* @param {String} [rdCountdownMessage=''] 輸入顯示倒數文字字串
* @returns {String} 回傳html字串
*/
function genVerifyHtml(rdMessage = '', rdCountdown = 'false', rdCountdownMessage = '') {
//tmp
let tmp = opt.verifyWebHtml
//replaceObj
tmp = replaceObj(tmp, {
'{rdWebName}': opt.webName,
'{rdWebDescription}': opt.webDescription,
'{rdWebUrl}': opt.webUrl,
'{rdLogo}': opt.verifyWebLogo,
'{rdMessage}': rdMessage, //驗證成功|驗證失敗
'{rdCountdown}': rdCountdown, //true|false
'{rdCountdownMessage}': rdCountdownMessage, //秒後自動前往網站
})
return tmp
}
function verifyCheckCodeAndGetHtml(checkCode) {
let pm = genPm()
//rdCountdownMessage
let rdCountdownMessage = opt.verifyWebMsgCount
verifyCheckCode(checkCode)
.then(function(msg) {
eeEmit('verifyCheckCodeAndGetHtml', { state: 'success', msg, checkCode })
pm.resolve(genVerifyHtml(msg, 'true', rdCountdownMessage))
})
.catch(function(err) {
eeEmit('verifyCheckCodeAndGetHtml', { state: 'error', msg: err, checkCode })
pm.resolve(genVerifyHtml(err, 'false', ''))
})
return pm
}
/**
* 評估驗證碼過期時間並回傳狀態
*
* @memberof WUserServer::Private
* @function verifyTokenExp
* @param {String} tokenExp
* @returns {Promise} 回傳Promise,resolve表示成功,reject代表錯誤並回傳錯誤訊息
*/
function verifyTokenExp(tokenExp) {
//可不用回傳promise, 為統一函數型態
let pm = genPm()
//check
if (!istimeTZ(tokenExp)) {
pm.reject('no expire time for token')
}
else {
//現在時間
let dnow = dayjs()
//token有效時間
let dtoken = fromStrDate(tokenExp)
//相差分鐘
let iMinutes = dtoken.diff(dnow, 'minute')
//check
if (iMinutes > 0) {
pm.resolve('ok')
}
else if (iMinutes > opt.timeTokenExp) {
console.log('verifyTokenExp error:', 'iMinutes=' + iMinutes, '>', 'timeTokenExp=' + opt.timeTokenExp)
pm.reject('invalid tokenExp')
}
else {
pm.reject('token expired')
}
}
return pm
}
let verifyTokenExpR = pm2resolve(verifyTokenExp)
async function isValidToken(token) {
let pm = genPm()
let r = null
//check
if (!isestr(token)) {
pm.reject('invalid token')
return pm
}
//find
let find = { token }
//selectOneR
r = await selectOneR(find)
//cehck
if (r.state === 'error') {
console.log('isValidToken catch:', r.msg, 'find:', find)
pm.reject('invalid token')
return pm
}
//user
let user = r.msg
//verifyTokenExpR
r = verifyTokenExpR(user.tokenExp)
//cehck
if (r.state === 'error') {
pm.reject(r.msg)
return pm
}
pm.resolve('ok')
return pm
}
let isValidTokenR = pm2resolve(isValidToken)
async function refreshTokenExp(token) {
let pm = genPm()
let r = null
//check
if (!isestr(token)) {
pm.reject('invalid token')
return pm
}
//find
let find = { token }
//selectOneR
r = await selectOneR(find)
//cehck
if (r.state === 'error') {
console.log('refreshTokenExp catch:', r.msg, 'find:', find)
pm.reject('invalid token')
return pm
}
//user
let user = r.msg
//verifyTokenExp
r = verifyTokenExp(user.tokenExp)
if (r.state === 'error') {
pm.reject(r.err)
return pm
}
//userModify
let userModify = {
id: user.id,
tokenExp: toStrDate(dayjs().add(opt.timeTokenExp, 'minute')),
}
//wsaveR
r = await wsaveR(userModify)
//cehck
if (r.state === 'error') {
console.log('refreshTokenExp catch:', r.msg, 'userModify:', userModify)
pm.reject('can not update token')
return pm
}
pm.resolve('ok')
return pm
}
let refreshTokenExpR = pm2resolve(refreshTokenExp)
/**
* 由token取得使用者資訊
*
* @memberof WUserServer::Private
* @function getUserFromToken
* @param {String} token
* @returns {Promise} 回傳Promise,resolve代表成功直接回傳使用者資訊物件,reject代表錯誤並回傳錯誤訊息
*/
async function getUserFromToken(token) {
let pm = genPm()
let r = null
//find
let find = { token }
//selectOneR
r = await selectOneR(find)
//cehck
if (r.state === 'error') {
console.log('getUserFromToken catch:', r.msg, 'find:', find)
pm.reject('invalid token')
return pm
}
//user
let user = r.msg
pm.resolve(user)
return pm
}
async function changePW({ email, pwEnc, pwEncNew }) {
let pm = genPm()
let r = null
//check
if (!isestr(email)) {
pm.reject('invalid email')
return pm
}
if (!isestr(pwEnc)) {
pm.reject('invalid old password')
return pm
}
if (!isestr(pwEncNew)) {
pm.reject('invalid new password')
return pm
}
//find
let find = { email } //先查詢email是否存在
//selectOneR
r = await selectOneR(find)
//cehck
if (r.state === 'error') {
console.log('changePW catch:', r.msg, 'find:', find)
pm.reject('email or password is incorrect') //找不到使用者, 回傳訊息統一為email或密碼不正確
return pm
}
//user
let user = r.msg
//check checkCode, 帳號需驗證後才能更改密碼
if (user.checkCode !== '') {
pm.reject('account is not verified')
return pm
}
//check active, 帳號需有效才能更改密碼
if (user.active !== defActive.activeYes) {
pm.reject('account is suspended')
return pm
}
//加密密碼加鹽再加密
if (opt.salt !== '') {
pwEnc = str2sha512(pwEnc + opt.salt)
pwEncNew = str2sha512(pwEncNew + opt.salt)
}
//check pwEnc, 確認密碼是否正確
if (user.pwEnc !== pwEnc) {
pm.reject('email or password is incorrect') //回傳訊息統一為email或密碼不正確
return pm
}
//userModify
let userModify = {
id: user.id,
pwEnc: pwEncNew
}
//wsaveR
r = await wsaveR(userModify)
//cehck
if (r.state === 'error') {
console.log('changePW catch:', r.msg, 'userModify:', userModify)
pm.reject('can not update password')
return pm
}
pm.resolve('ok')
return pm
}
let changePWR = pm2resolve(changePW)
async function resetPW({ email, pwEncNew }) {
let pm = genPm()
let r = null
//check
if (!isestr(email)) {
pm.reject('invalid email')
return pm
}
if (!isestr(pwEncNew)) {
pm.reject('invalid new password')
return pm
}
//find
let find = { email } //先查詢email是否存在
//selectOneR
r = await selectOneR(find)
//cehck
if (r.state === 'error') {
console.log('resetPW catch:', r.msg, 'find:', find)
pm.reject('email is incorrect')
return pm
}
//user
let user = r.msg
//check checkCode, 帳號不需驗證亦可申請重設密碼驗證信
//check active, 帳號需有效才能申請重設密碼驗證信
if (user.active !== defActive.activeYes) {
pm.reject('account is suspended')
return pm
}
//加密密碼加鹽再加密
if (opt.salt !== '') {
pwEncNew = str2sha512(pwEncNew + opt.salt)
}
//checkCode
let checkCode = genID()
//userModify
let userModify = {
id: user.id,
pwEnc: pwEncNew,
checkCode
}
//wsaveR
r = await wsaveR(userModify)
//cehck
if (r.state === 'error') {
console.log('resetPW catch:', r.msg, 'userModify:', userModify)
pm.reject('can not update password')
return pm
}
//urlCheckCode
let urlCheckCode = `${opt.authUrl}?checkCode=${checkCode}`
//sendEmailWhenResetPWR, 寄信給使用者驗證碼(checkCode), 點擊後才視為驗證成功
r = await sendEmailWhenResetPWR(user.name, urlCheckCode, user.email)
//cehck
if (r.state === 'error') {
console.log('resetPW catch:', r.msg, 'emailObj:', { name: user.name, urlCheckCode, email: user.email })
pm.reject('can not send email')
return pm
}
pm.resolve('ok')
return pm
}
let resetPWR = pm2resolve(resetPW)
async function modifyInfor({ token, user }) {
let pm = genPm()
let r = null
//isValidTokenR
r = await isValidTokenR(token)
//cehck
if (r.state === 'error') {
pm.reject(r.msg)
return pm
}
//verifyProp
r = verifyProp(user)
if (r.state === 'error') {
pm.reject(r.msg)
return pm
}
//getUserFromToken
let userOld = await getUserFromToken(token)
//userModify
let userModify = {
id: userOld.id,
}
//modify
let bModify = false
each(sc, function(v, k) {
if (v.canModify) {
if (user[k] && userOld[k] !== user[k]) {
bModify = true
userModify[k] = user[k]
}
}
})
//check
if (!bModify) {
pm.resolve('no change')
return pm
}
//wsaveR
r = await wsaveR(userModify)
//cehck
if (r.state === 'error') {
console.log('modifyInfor catch:', r.msg, 'userModify:', userModify)
pm.reject('can not modify')
return pm
}
pm.resolve('ok')
return pm
}
let modifyInforR = pm2resolve(modifyInfor)
/**
* 寄送email
*
* @param {String} emTitle 輸入郵件名稱字串
* @param {String} emContent 輸入郵件訊息字串
* @param {String|Array} toEmails 輸入收件人email字串或陣列
* @param {String|Array} [toEmailsCC=[]] 輸入副本收件人email字串或陣列,預設[]
* @param {String|Array} [toEmailsBCC=[]] 輸入密件副本收件人email字串或陣列,預設[]
* @returns {Promise} 回傳Promise,resolve回傳成功訊息,reject回傳錯誤訊息
*/
function sendEmail(emTitle, emContent, toEmails, toEmailsCC = [], toEmailsBCC = []) {
//optEmail
let optEmail = {
srcName: opt.emSenderName,
srcEmail: opt.emSenderEmail,
srcPW: opt.emSenderPW,
emTitle,
emContent,
toEmails,
toEmailsCC,
toEmailsBCC,
}
//WEmail
return new WEmail(optEmail)
}
/**
* 使用模板呈現內文並寄送email
*
* @param {String} emTitle 輸入郵件名稱字串
* @param {String} emContent 輸入郵件訊息字串
* @param {String|Array} toEmails 輸入收件人email字串或陣列
* @param {String|Array} [toEmailsCC=[]] 輸入副本收件人email字串或陣列,預設[]
* @param {String|Array} [toEmailsBCC=[]] 輸入密件副本收件人email字串或陣列,預設[]
* @returns {Promise} 回傳Promise,resolve回傳成功訊息,reject回傳錯誤訊息
*/
function sendEmailByLetter(emTitle, emContent, toEmails, toEmailsCC, toEmailsBCC) {
//tmp
let tmp = opt.emLetterHtml
//webName, webUrl, emLetterLinks
let webName = opt.webName
let webUrl = opt.webUrl
let emLetterLinks = opt.emLetterLinks
if (webName !== '' && webUrl !== '') {
emLetterLinks = concat({ name: webName, url: webUrl }, emLetterLinks)
}
emLetterLinks = map(emLetterLinks, function(v) {
let name = get(v, 'name', '')
let url = get(v, 'url', '')
return `<a style="text-decoration:none" href="${url}" target="_blank" >${name}</a>`
})
emLetterLinks = `| ${join(emLetterLinks, ' | ')} |`
//replaceObj
tmp = replaceObj(tmp, {
'{emMessage}': emContent,
'{emWebName}': webName,
'{emWebUrl}': webUrl,
'{emWebDescription}': opt.webDescription,
'{emLetterDoNotReplayMessage}': opt.emLetterDoNotReplayMessage,
'{emLetterTeamMessage}': opt.emLetterTeamMessage,
'{emLetterLinks}': emLetterLinks,
})
//sendEmail
return sendEmail(emTitle, tmp, toEmails, toEmailsCC, toEmailsBCC)
}
function sendEmailWhenSignUp(name, urlCheckCode, toEmails) {
//emTitle
let emTitle = opt.emSignUpTitle
//tmp
let tmp = opt.emSignUpHtml
//replaceObj
tmp = replaceObj(tmp, {
'{name}': name,
'{urlCheckCode}': urlCheckCode,
})
return sendEmailByLetter(emTitle, tmp, toEmails, [], opt.emBCC)
}
let sendEmailWhenSignUpR = pm2resolve(sendEmailWhenSignUp)
function sendEmailWhenResetPW(name, urlCheckCode, toEmails) {
//emTitle
let emTitle = opt.emResetPWTitle
//tmp
let tmp = opt.emResetPWHtml
//replaceObj
tmp = replaceObj(tmp, {
'{name}': name,
'{urlCheckCode}': urlCheckCode,
})
return sendEmailByLetter(emTitle, tmp, toEmails, [], opt.emBCC)
}
let sendEmailWhenResetPWR = pm2resolve(sendEmailWhenResetPW)
//funcs
let funcs = {
/**
* 公開用查詢使用者
*
* @memberof WUserServer
* @function select
* @param {Object} inp 輸入token與查詢物件
* @param {String} inp.token 輸入使用者token字串
* @param {Object} [inp.find={}] 輸入查詢物件,預設{}
* @returns {Promise} 回傳Promise,resolve回傳物件,成功則state為'success',於msg為使用者資料陣列,失敗則state為'error',於msg顯示錯誤訊息,無reject
*/
select: function(inp) {
return pmWithEmit(selectPublicR, 'selectPublic')(inp)
},
/**
* 使用者註冊新帳號
*
* 1.使用者用email註冊帳號,且email於全系統唯一不得重複,已申請過不得再被使用
*
* 2.註冊之後,使用者需收信驗證通過,帳號才會有效(active為activeYes)
*
* 3.若使用者忘記點擊或沒收到信,則該email不能再被申請,需由使用者用忘記密碼功能,系統重新寄送驗證信後進行驗證
*
* 4.被停權者(active非activeYes),需由系統管理員復權
*
* @memberof WUserServer
* @function signUp
* @param {Object} user 輸入使用者物件
* @param {String} user.email 輸入email字串
* @param {String} user.pwEnc 輸入密碼加密後字串
* @param {String} [user.address=''] 輸入地址字串
* @param {String} [user.phone=''] 輸入電話字串
* @param {String} [user.organization=''] 輸入組織字串
* @param {String} [user.position=''] 輸入職位字串
* @returns {Promise} 回傳Promise,resolve回傳物件,成功則state為'success',於msg顯示使用者id,失敗則state為'error',於msg顯示錯誤訊息,無reject
*/
signUp: function(user) {
return pmWithEmit(signUpR, 'signUp')(user)
},
/**
* 使用者登入
*
* 1.使用者以email與密碼為登入,且帳號需為有效(active為activeYes)
*
* 2.帳號需驗證後才能登入,使用者需先收信驗證
*
* 3.無驗證則重寄驗證信。若使用者忘記點擊或沒收到信,則該email不能再被申請,需由使用者用忘記密碼功能,系統重新寄送驗證信後進行驗證
*
* 4.忘記密碼則重寄驗證信。若使用者登入失敗或忘記密碼,請使用者確認自己email是否正確。若是忘記密碼,則使用忘記密碼功能,系統重新寄送驗證信後進行驗證
*
* 5.支援多點登入。若使用者重複或多點登入時,且token亦於有效時限內,則直接給予當前token而不再另外產生,避免造成前次token失效
*
* @memberof WUserServer
* @function logIn
* @param {Object} user 輸入使用者物件
* @param {String} user.email 輸入email字串
* @param {String} user.pwEnc 輸入密碼加密後字串
* @returns {Promise} 回傳Promise,resolve回傳物件,成功則state為'success',於msg顯示使用者token,失敗則state為'error',於msg顯示錯誤訊息,無reject
*/
logIn: function(user) {
return pmWithEmit(logInR, 'logIn')(user)
},
/**
* 更新使用者token過期時間
*
* 1.token需仍於有效時限內才能刷新
*
* @memberof WUserServer
* @function refreshTokenExp
* @param {String} token 輸入使用者token字串
* @returns {Promise} 回傳Promise,resolve回傳物件,成功則state為'success',失敗則state為'error',於msg顯示錯誤訊息,無reject
*/
refreshTokenExp: function(token) {
return pmWithEmit(refreshTokenExpR, 'refreshTokenExp')(token)
},
/**
* 確認使用者token是否有效
*
* 1.確認使用者token是否仍於有效時限內
*
* @memberof WUserServer
* @function isValidToken
* @param {String} token 輸入使用者token字串
* @returns {Promise} 回傳Promise,resolve回傳物件,成功則state為'success',失敗則state為'error',於msg顯示錯誤訊息,無reject
*/
isValidToken: function(token) {
return pmWithEmit(isValidTokenR, 'isValidToken')(token)
},
/**
* 使用者登出
*
* 1.使用者憑token登出
*
* @memberof WUserServer
* @function logOut
* @param {String} token 輸入使用者token字串
* @returns {Promise} 回傳Promise,resolve回傳物件,成功則state為'success',失敗則state為'error',於msg顯示錯誤訊息,無reject
*/
logOut: function(token) {
return pmWithEmit(logOutR, 'logOut')(token)
},
/**
* 使用者變更密碼
*
* 1.帳號需為有效(active為activeYes)
*
* 2.帳號需驗證後才能更改密碼
*
* @memberof WUserServer
* @function changePW
* @param {Object} user 輸入使用者物件
* @param {String} user.email 輸入email字串
* @param {String} user.pwEnc 輸入密碼加密後字串
* @returns {Promise} 回傳Promise,resolve回傳物件,成功則state為'success',失敗則state為'error',於msg顯示錯誤訊息,無reject
*/
changePW: function(user) {
return pmWithEmit(changePWR, 'changePW')(user)
},
/**
* 使用者重設密碼
*
* 1.帳號需為有效(active為activeYes)
*
* 2.使用者忘記密碼時,可直接設定新密碼並寄送驗證信
*
* 2.帳號不需通過驗證也可申請變更密碼,當使用者申請帳號但忘記收信驗證時,以此可重新設定新密碼並收信驗證
*
* @memberof WUserServer
* @function resetPW
* @param {Object} user 輸入使用者物件
* @param {String} user.email 輸入email字串
* @param {String} user.pwEncNew 輸入新密碼加密後字串
* @returns {Promise} 回傳Promise,resolve回傳物件,成功則state為'success',失敗則state為'error',於msg顯示錯誤訊息,無reject
*/
resetPW: function(user) {
return pmWithEmit(resetPWR, 'resetPW')(user)
},
/**
* 使用者更改自己資料
*
* @memberof WUserServer
* @function modifyInfor
* @param {Object} inp 輸入token與查詢物件
* @param {String} inp.token 輸入使用者token字串
* @param {Object} [inp.user={}] 輸入使用者資訊物件,預設{}
* @returns {Promise} 回傳Promise,resolve回傳物件,成功則state為'success',失敗則state為'error',於msg顯示錯誤訊息,無reject
*/
modifyInfor: function(inp) {
return pmWithEmit(modifyInforR, 'modifyInfor')(inp)
},
}
each(opt.funcs, function(v, k) {
if (haskey(funcs, k)) {
console.log('invalid func: ' + k)
}
else {
funcs[k] = opt.funcs[k]
}
})
//routes
let routes = [
{
method: 'GET',
path: '/auth',
handler: async function (req, res) {
//console.log(`Server[port:${opt.port}][api:auth]:`, 'checkCode: ' + req.query.checkCode)
//checkCode
let checkCode = req.query.checkCode
/**
* 接收使用者點擊信內提供的驗證碼連結
*
* 1.判斷並清除使用者驗證瑪
*
* 2.成功與失敗會回傳顯示網站html
*
* @memberof WUserServer::Private
* @function verifyCheckCodeAndGetHtml
* @param {String} checkCode
* @returns {Promise} 回傳Promise,resolve回傳驗證結果html,無reject
*/
return verifyCheckCodeAndGetHtml(checkCode)
}
},
]
each(opt.routes, function(v) {
routes.push(v)
})
//merge opt
opt = {
...opt,
// filterFuncs: function(token, funcs) {
// return new Promise(function(resolve, reject) {
// funcs = funcs.filter(function(v) {
// return v.indexOf('Hide') < 0
// })
// resolve(funcs)
// })
// },
onClientChange: function(clients, opt) {
//console.log(`Server[port:${opt.port}] now clients: ${clients.length}`)
eeEmit('clientChange', clients.length, clients)
},
funcs,
routes,
}
//new
new WComorHapiServer(opt)
return ee
}
export default WUserServer