444 lines
10 KiB
JavaScript
444 lines
10 KiB
JavaScript
const {
|
||
isValidString,
|
||
getType
|
||
} = require('./utils.js')
|
||
const {
|
||
ERROR
|
||
} = require('./error')
|
||
|
||
const baseValidator = Object.create(null)
|
||
|
||
baseValidator.username = function (username) {
|
||
const errCode = ERROR.INVALID_USERNAME
|
||
if (!isValidString(username)) {
|
||
return {
|
||
errCode
|
||
}
|
||
}
|
||
if (/^\d+$/.test(username)) {
|
||
// 用户名不能为纯数字
|
||
return {
|
||
errCode
|
||
}
|
||
};
|
||
if (!/^[a-zA-Z0-9_-]+$/.test(username)) {
|
||
// 用户名仅能使用数字、字母、“_”及“-”
|
||
return {
|
||
errCode
|
||
}
|
||
}
|
||
}
|
||
|
||
baseValidator.password = function (password) {
|
||
const errCode = ERROR.INVALID_PASSWORD
|
||
if (!isValidString(password)) {
|
||
return {
|
||
errCode
|
||
}
|
||
}
|
||
if (password.length < 6) {
|
||
// 密码长度不能小于6
|
||
return {
|
||
errCode
|
||
}
|
||
}
|
||
}
|
||
|
||
baseValidator.mobile = function (mobile) {
|
||
const errCode = ERROR.INVALID_MOBILE
|
||
if (getType(mobile) !== 'string') {
|
||
return {
|
||
errCode
|
||
}
|
||
}
|
||
if (mobile && !/^1\d{10}$/.test(mobile)) {
|
||
return {
|
||
errCode
|
||
}
|
||
}
|
||
}
|
||
|
||
baseValidator.email = function (email) {
|
||
const errCode = ERROR.INVALID_EMAIL
|
||
if (getType(email) !== 'string') {
|
||
return {
|
||
errCode
|
||
}
|
||
}
|
||
if (email && !/@/.test(email)) {
|
||
return {
|
||
errCode
|
||
}
|
||
}
|
||
}
|
||
|
||
baseValidator.nickname = function (nickname) {
|
||
const errCode = ERROR.INVALID_NICKNAME
|
||
if (nickname.indexOf('@') !== -1) {
|
||
// 昵称不允许含@
|
||
return {
|
||
errCode
|
||
}
|
||
};
|
||
if (/^\d+$/.test(nickname)) {
|
||
// 昵称不能为纯数字
|
||
return {
|
||
errCode
|
||
}
|
||
};
|
||
if (nickname.length > 100) {
|
||
// 昵称不可超过100字符
|
||
return {
|
||
errCode
|
||
}
|
||
}
|
||
}
|
||
|
||
const baseType = ['string', 'boolean', 'number', 'null'] // undefined不会由客户端提交上来
|
||
|
||
baseType.forEach((type) => {
|
||
baseValidator[type] = function (val) {
|
||
if (getType(val) === type) {
|
||
return
|
||
}
|
||
return {
|
||
errCode: ERROR.INVALID_PARAM
|
||
}
|
||
}
|
||
})
|
||
|
||
function tokenize(name) {
|
||
let i = 0
|
||
const result = []
|
||
let token = ''
|
||
while (i < name.length) {
|
||
const char = name[i]
|
||
switch (char) {
|
||
case '|':
|
||
case '<':
|
||
case '>':
|
||
token && result.push(token)
|
||
result.push(char)
|
||
token = ''
|
||
break
|
||
default:
|
||
token += char
|
||
break
|
||
}
|
||
i++
|
||
if (i === name.length && token) {
|
||
result.push(token)
|
||
}
|
||
}
|
||
return result
|
||
}
|
||
|
||
/**
|
||
* 处理validator名
|
||
* @param {string} name
|
||
*/
|
||
function parseValidatorName(name) {
|
||
const tokenList = tokenize(name)
|
||
let i = 0
|
||
let currentToken = tokenList[i]
|
||
const result = {
|
||
type: 'root',
|
||
children: [],
|
||
parent: null
|
||
}
|
||
let lastRealm = result
|
||
while (currentToken) {
|
||
switch (currentToken) {
|
||
case 'array': {
|
||
const currentRealm = {
|
||
type: 'array',
|
||
children: [],
|
||
parent: lastRealm
|
||
}
|
||
lastRealm.children.push(currentRealm)
|
||
lastRealm = currentRealm
|
||
break
|
||
}
|
||
case '<':
|
||
if (lastRealm.type !== 'array') {
|
||
throw new Error('Invalid validator token "<"')
|
||
}
|
||
break
|
||
case '>':
|
||
if (lastRealm.type !== 'array') {
|
||
throw new Error('Invalid validator token ">"')
|
||
}
|
||
lastRealm = lastRealm.parent
|
||
break
|
||
case '|':
|
||
if (lastRealm.type !== 'array' && lastRealm.type !== 'root') {
|
||
throw new Error('Invalid validator token "|"')
|
||
}
|
||
break
|
||
default:
|
||
lastRealm.children.push({
|
||
type: currentToken
|
||
})
|
||
break
|
||
}
|
||
i++
|
||
currentToken = tokenList[i]
|
||
}
|
||
return result
|
||
}
|
||
|
||
function getRuleCategory(rule) {
|
||
switch (rule.type) {
|
||
case 'array':
|
||
return 'array'
|
||
case 'root':
|
||
return 'root'
|
||
default:
|
||
return 'base'
|
||
}
|
||
}
|
||
|
||
|
||
// 特殊符号 https://www.ibm.com/support/pages/password-strength-rules ~!@#$%^&*_-+=`|\(){}[]:;"'<>,.?/
|
||
// const specialChar = '~!@#$%^&*_-+=`|\(){}[]:;"\'<>,.?/'
|
||
// const specialCharRegExp = /^[~!@#$%^&*_\-+=`|\\(){}[\]:;"'<>,.?/]$/
|
||
// for (let i = 0, arr = specialChar.split(''); i < arr.length; i++) {
|
||
// const char = arr[i]
|
||
// if (!specialCharRegExp.test(char)) {
|
||
// throw new Error('check special character error: ' + char)
|
||
// }
|
||
// }
|
||
|
||
// 密码强度表达式
|
||
const passwordRules = {
|
||
// 密码必须包含大小写字母、数字和特殊符号
|
||
super: /^(?=.*[0-9])(?=.*[a-z])(?=.*[A-Z])(?=.*[~!@#$%^&*_\-+=`|\\(){}[\]:;"'<>,.?/])[0-9a-zA-Z~!@#$%^&*_\-+=`|\\(){}[\]:;"'<>,.?/]{8,16}$/,
|
||
// 密码必须包含字母、数字和特殊符号
|
||
strong: /^(?=.*[0-9])(?=.*[a-zA-Z])(?=.*[~!@#$%^&*_\-+=`|\\(){}[\]:;"'<>,.?/])[0-9a-zA-Z~!@#$%^&*_\-+=`|\\(){}[\]:;"'<>,.?/]{8,16}$/,
|
||
// 密码必须为字母、数字和特殊符号任意两种的组合
|
||
medium: /^(?![0-9]+$)(?![a-zA-Z]+$)(?![~!@#$%^&*_\-+=`|\\(){}[\]:;"'<>,.?/]+$)[0-9a-zA-Z~!@#$%^&*_\-+=`|\\(){}[\]:;"'<>,.?/]{8,16}$/,
|
||
// 密码必须包含字母和数字
|
||
weak: /^(?=.*[0-9])(?=.*[a-zA-Z])[0-9a-zA-Z~!@#$%^&*_\-+=`|\\(){}[\]:;"'<>,.?/]{6,16}$/,
|
||
|
||
}
|
||
|
||
function createPasswordVerifier({
|
||
passwordStrength = ''
|
||
} = {}) {
|
||
return function (password) {
|
||
const passwordRegExp = passwordRules[passwordStrength]
|
||
if (!passwordRegExp) {
|
||
throw new Error('Invalid password strength config: ' + passwordStrength)
|
||
}
|
||
const errCode = ERROR.INVALID_PASSWORD
|
||
if (!isValidString(password)) {
|
||
return {
|
||
errCode
|
||
}
|
||
}
|
||
if (!passwordRegExp.test(password)) {
|
||
return {
|
||
errCode: errCode + '-' + passwordStrength
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
function isEmpty(value) {
|
||
return value === undefined ||
|
||
value === null ||
|
||
(typeof value === 'string' && value.trim() === '')
|
||
}
|
||
|
||
class Validator {
|
||
constructor({
|
||
passwordStrength = ''
|
||
} = {}) {
|
||
this.baseValidator = baseValidator
|
||
this.customValidator = Object.create(null)
|
||
if (passwordStrength) {
|
||
this.mixin(
|
||
'password',
|
||
createPasswordVerifier({
|
||
passwordStrength
|
||
})
|
||
)
|
||
}
|
||
}
|
||
|
||
mixin(type, handler) {
|
||
this.customValidator[type] = handler
|
||
}
|
||
|
||
getRealBaseValidator(type) {
|
||
return this.customValidator[type] || this.baseValidator[type]
|
||
}
|
||
|
||
|
||
_isMatchUnionType(val, rule) {
|
||
if (!rule.children || rule.children.length === 0) {
|
||
return true
|
||
}
|
||
const children = rule.children
|
||
for (let i = 0; i < children.length; i++) {
|
||
const child = children[i]
|
||
const category = getRuleCategory(child)
|
||
let pass = false
|
||
switch (category) {
|
||
case 'base':
|
||
pass = this._isMatchBaseType(val, child)
|
||
break
|
||
case 'array':
|
||
pass = this._isMatchArrayType(val, child)
|
||
break
|
||
default:
|
||
break
|
||
}
|
||
if (pass) {
|
||
return true
|
||
}
|
||
}
|
||
return false
|
||
}
|
||
|
||
_isMatchBaseType(val, rule) {
|
||
const method = this.getRealBaseValidator(rule.type)
|
||
if (typeof method !== 'function') {
|
||
throw new Error(`invalid schema type: ${rule.type}`)
|
||
}
|
||
const validateRes = method(val)
|
||
if (validateRes && validateRes.errCode) {
|
||
return false
|
||
}
|
||
return true
|
||
}
|
||
|
||
_isMatchArrayType(arr, rule) {
|
||
if (getType(arr) !== 'array') {
|
||
return false
|
||
}
|
||
if (rule.children && rule.children.length && arr.some(item => !this._isMatchUnionType(item, rule))) {
|
||
return false
|
||
}
|
||
return true
|
||
}
|
||
|
||
get validator() {
|
||
const _this = this
|
||
return new Proxy({}, {
|
||
get: (_, prop) => {
|
||
if (typeof prop !== 'string') {
|
||
return
|
||
}
|
||
const realBaseValidator = this.getRealBaseValidator(prop)
|
||
if (realBaseValidator) {
|
||
return realBaseValidator
|
||
}
|
||
const rule = parseValidatorName(prop)
|
||
return function (val) {
|
||
if (!_this._isMatchUnionType(val, rule)) {
|
||
return {
|
||
errCode: ERROR.INVALID_PARAM
|
||
}
|
||
}
|
||
}
|
||
}
|
||
})
|
||
}
|
||
|
||
validate(value = {}, schema = {}) {
|
||
for (const schemaKey in schema) {
|
||
let schemaValue = schema[schemaKey]
|
||
if (getType(schemaValue) === 'string') {
|
||
schemaValue = {
|
||
required: true,
|
||
type: schemaValue
|
||
}
|
||
}
|
||
const {
|
||
required,
|
||
type
|
||
} = schemaValue
|
||
// value内未传入了schemaKey或对应值为undefined
|
||
if (isEmpty(value[schemaKey])) {
|
||
if (required) {
|
||
return {
|
||
errCode: ERROR.PARAM_REQUIRED,
|
||
errMsgValue: {
|
||
param: schemaKey
|
||
},
|
||
schemaKey
|
||
}
|
||
} else {
|
||
//delete value[schemaKey]
|
||
continue
|
||
}
|
||
}
|
||
const validateMethod = this.validator[type]
|
||
if (!validateMethod) {
|
||
throw new Error(`invalid schema type: ${type}`)
|
||
}
|
||
const validateRes = validateMethod(value[schemaKey])
|
||
if (validateRes) {
|
||
validateRes.schemaKey = schemaKey
|
||
return validateRes
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
function checkClientInfo(clientInfo) {
|
||
const stringNotRequired = {
|
||
required: false,
|
||
type: 'string'
|
||
}
|
||
const numberNotRequired = {
|
||
required: false,
|
||
type: 'number'
|
||
}
|
||
const numberOrStringNotRequired = {
|
||
required: false,
|
||
type: 'number|string'
|
||
}
|
||
const schema = {
|
||
uniPlatform: 'string',
|
||
appId: 'string',
|
||
deviceId: stringNotRequired,
|
||
osName: stringNotRequired,
|
||
locale: stringNotRequired,
|
||
clientIP: stringNotRequired,
|
||
appName: stringNotRequired,
|
||
appVersion: stringNotRequired,
|
||
appVersionCode: numberOrStringNotRequired,
|
||
channel: numberOrStringNotRequired,
|
||
userAgent: stringNotRequired,
|
||
uniIdToken: stringNotRequired,
|
||
deviceBrand: stringNotRequired,
|
||
deviceModel: stringNotRequired,
|
||
osVersion: stringNotRequired,
|
||
osLanguage: stringNotRequired,
|
||
osTheme: stringNotRequired,
|
||
romName: stringNotRequired,
|
||
romVersion: stringNotRequired,
|
||
devicePixelRatio: numberNotRequired,
|
||
windowWidth: numberNotRequired,
|
||
windowHeight: numberNotRequired,
|
||
screenWidth: numberNotRequired,
|
||
screenHeight: numberNotRequired
|
||
}
|
||
const validateRes = new Validator().validate(clientInfo, schema)
|
||
if (validateRes) {
|
||
if (validateRes.errCode === ERROR.PARAM_REQUIRED) {
|
||
console.warn('- 如果使用HBuilderX运行本地云函数/云对象功能时出现此提示,请改为使用客户端调用本地云函数方式调试,或更新HBuilderX到3.4.12及以上版本。\n- 如果是缺少clientInfo.appId,请检查项目manifest.json内是否配置了DCloud AppId')
|
||
throw new Error(`"clientInfo.${validateRes.schemaKey}" is required.`)
|
||
} else {
|
||
throw new Error(`Invalid client info: clienInfo.${validateRes.schemaKey}`)
|
||
}
|
||
}
|
||
}
|
||
|
||
module.exports = {
|
||
Validator,
|
||
checkClientInfo
|
||
}
|