首次完整推送,
V:1.20240808.006
This commit is contained in:
@ -0,0 +1,108 @@
|
||||
const db = uniCloud.database()
|
||||
const dbCmd = db.command
|
||||
const userCollectionName = 'uni-id-users'
|
||||
const userCollection = db.collection(userCollectionName)
|
||||
const verifyCollectionName = 'opendb-verify-codes'
|
||||
const verifyCollection = db.collection(verifyCollectionName)
|
||||
const deviceCollectionName = 'uni-id-device'
|
||||
const deviceCollection = db.collection(deviceCollectionName)
|
||||
const openDataCollectionName = 'opendb-open-data'
|
||||
const openDataCollection = db.collection(openDataCollectionName)
|
||||
const frvLogsCollectionName = 'opendb-frv-logs'
|
||||
const frvLogsCollection = db.collection(frvLogsCollectionName)
|
||||
|
||||
const USER_IDENTIFIER = {
|
||||
_id: 'uid',
|
||||
username: 'username',
|
||||
mobile: 'mobile',
|
||||
email: 'email',
|
||||
wx_unionid: 'wechat-account',
|
||||
'wx_openid.app': 'wechat-account',
|
||||
'wx_openid.mp': 'wechat-account',
|
||||
'wx_openid.h5': 'wechat-account',
|
||||
'wx_openid.web': 'wechat-account',
|
||||
qq_unionid: 'qq-account',
|
||||
'qq_openid.app': 'qq-account',
|
||||
'qq_openid.mp': 'qq-account',
|
||||
ali_openid: 'alipay-account',
|
||||
apple_openid: 'alipay-account',
|
||||
identities: 'idp'
|
||||
}
|
||||
|
||||
const USER_STATUS = {
|
||||
NORMAL: 0,
|
||||
BANNED: 1,
|
||||
AUDITING: 2,
|
||||
AUDIT_FAILED: 3,
|
||||
CLOSED: 4
|
||||
}
|
||||
|
||||
const CAPTCHA_SCENE = {
|
||||
REGISTER: 'register',
|
||||
LOGIN_BY_PWD: 'login-by-pwd',
|
||||
LOGIN_BY_SMS: 'login-by-sms',
|
||||
RESET_PWD_BY_SMS: 'reset-pwd-by-sms',
|
||||
RESET_PWD_BY_EMAIL: 'reset-pwd-by-email',
|
||||
SEND_SMS_CODE: 'send-sms-code',
|
||||
SEND_EMAIL_CODE: 'send-email-code',
|
||||
BIND_MOBILE_BY_SMS: 'bind-mobile-by-sms',
|
||||
SET_PWD_BY_SMS: 'set-pwd-by-sms'
|
||||
}
|
||||
|
||||
const LOG_TYPE = {
|
||||
LOGOUT: 'logout',
|
||||
LOGIN: 'login',
|
||||
REGISTER: 'register',
|
||||
RESET_PWD_BY_SMS: 'reset-pwd',
|
||||
RESET_PWD_BY_EMAIL: 'reset-pwd',
|
||||
BIND_MOBILE: 'bind-mobile',
|
||||
BIND_WEIXIN: 'bind-weixin',
|
||||
BIND_QQ: 'bind-qq',
|
||||
BIND_APPLE: 'bind-apple',
|
||||
BIND_ALIPAY: 'bind-alipay',
|
||||
UNBIND_WEIXIN: 'unbind-weixin',
|
||||
UNBIND_QQ: 'unbind-qq',
|
||||
UNBIND_ALIPAY: 'unbind-alipay',
|
||||
UNBIND_APPLE: 'unbind-apple'
|
||||
}
|
||||
|
||||
const SMS_SCENE = {
|
||||
LOGIN_BY_SMS: 'login-by-sms',
|
||||
RESET_PWD_BY_SMS: 'reset-pwd-by-sms',
|
||||
BIND_MOBILE_BY_SMS: 'bind-mobile-by-sms',
|
||||
SET_PWD_BY_SMS: 'set-pwd-by-sms'
|
||||
}
|
||||
|
||||
const EMAIL_SCENE = {
|
||||
REGISTER: 'register',
|
||||
LOGIN_BY_EMAIL: 'login-by-email',
|
||||
RESET_PWD_BY_EMAIL: 'reset-pwd-by-email',
|
||||
BIND_EMAIL: 'bind-email'
|
||||
}
|
||||
|
||||
const REAL_NAME_STATUS = {
|
||||
NOT_CERTIFIED: 0,
|
||||
WAITING_CERTIFIED: 1,
|
||||
CERTIFIED: 2,
|
||||
CERTIFY_FAILED: 3
|
||||
}
|
||||
|
||||
const EXTERNAL_DIRECT_CONNECT_PROVIDER = 'externalDirectConnect'
|
||||
|
||||
module.exports = {
|
||||
db,
|
||||
dbCmd,
|
||||
userCollection,
|
||||
verifyCollection,
|
||||
deviceCollection,
|
||||
openDataCollection,
|
||||
frvLogsCollection,
|
||||
USER_IDENTIFIER,
|
||||
USER_STATUS,
|
||||
CAPTCHA_SCENE,
|
||||
LOG_TYPE,
|
||||
SMS_SCENE,
|
||||
EMAIL_SCENE,
|
||||
REAL_NAME_STATUS,
|
||||
EXTERNAL_DIRECT_CONNECT_PROVIDER
|
||||
}
|
@ -0,0 +1,70 @@
|
||||
const ERROR = {
|
||||
ACCOUNT_EXISTS: 'uni-id-account-exists',
|
||||
ACCOUNT_NOT_EXISTS: 'uni-id-account-not-exists',
|
||||
ACCOUNT_NOT_EXISTS_IN_CURRENT_APP: 'uni-id-account-not-exists-in-current-app',
|
||||
ACCOUNT_CONFLICT: 'uni-id-account-conflict',
|
||||
ACCOUNT_BANNED: 'uni-id-account-banned',
|
||||
ACCOUNT_AUDITING: 'uni-id-account-auditing',
|
||||
ACCOUNT_AUDIT_FAILED: 'uni-id-account-audit-failed',
|
||||
ACCOUNT_CLOSED: 'uni-id-account-closed',
|
||||
CAPTCHA_REQUIRED: 'uni-id-captcha-required',
|
||||
PASSWORD_ERROR: 'uni-id-password-error',
|
||||
PASSWORD_ERROR_EXCEED_LIMIT: 'uni-id-password-error-exceed-limit',
|
||||
INVALID_USERNAME: 'uni-id-invalid-username',
|
||||
INVALID_PASSWORD: 'uni-id-invalid-password',
|
||||
INVALID_PASSWORD_SUPER: 'uni-id-invalid-password-super',
|
||||
INVALID_PASSWORD_STRONG: 'uni-id-invalid-password-strong',
|
||||
INVALID_PASSWORD_MEDIUM: 'uni-id-invalid-password-medium',
|
||||
INVALID_PASSWORD_WEAK: 'uni-id-invalid-password-weak',
|
||||
INVALID_MOBILE: 'uni-id-invalid-mobile',
|
||||
INVALID_EMAIL: 'uni-id-invalid-email',
|
||||
INVALID_NICKNAME: 'uni-id-invalid-nickname',
|
||||
INVALID_PARAM: 'uni-id-invalid-param',
|
||||
PARAM_REQUIRED: 'uni-id-param-required',
|
||||
GET_THIRD_PARTY_ACCOUNT_FAILED: 'uni-id-get-third-party-account-failed',
|
||||
GET_THIRD_PARTY_USER_INFO_FAILED: 'uni-id-get-third-party-user-info-failed',
|
||||
MOBILE_VERIFY_CODE_ERROR: 'uni-id-mobile-verify-code-error',
|
||||
EMAIL_VERIFY_CODE_ERROR: 'uni-id-email-verify-code-error',
|
||||
ADMIN_EXISTS: 'uni-id-admin-exists',
|
||||
PERMISSION_ERROR: 'uni-id-permission-error',
|
||||
SYSTEM_ERROR: 'uni-id-system-error',
|
||||
SET_INVITE_CODE_FAILED: 'uni-id-set-invite-code-failed',
|
||||
INVALID_INVITE_CODE: 'uni-id-invalid-invite-code',
|
||||
CHANGE_INVITER_FORBIDDEN: 'uni-id-change-inviter-forbidden',
|
||||
BIND_CONFLICT: 'uni-id-bind-conflict',
|
||||
UNBIND_FAIL: 'uni-id-unbind-failed',
|
||||
UNBIND_NOT_SUPPORTED: 'uni-id-unbind-not-supported',
|
||||
UNBIND_UNIQUE_LOGIN: 'uni-id-unbind-unique-login',
|
||||
UNBIND_PASSWORD_NOT_EXISTS: 'uni-id-unbind-password-not-exists',
|
||||
UNBIND_MOBILE_NOT_EXISTS: 'uni-id-unbind-mobile-not-exists',
|
||||
UNSUPPORTED_REQUEST: 'uni-id-unsupported-request',
|
||||
ILLEGAL_REQUEST: 'uni-id-illegal-request',
|
||||
CONFIG_FIELD_REQUIRED: 'uni-id-config-field-required',
|
||||
CONFIG_FIELD_INVALID: 'uni-id-config-field-invalid',
|
||||
FRV_FAIL: 'uni-id-frv-fail',
|
||||
FRV_PROCESSING: 'uni-id-frv-processing',
|
||||
REAL_NAME_VERIFIED: 'uni-id-realname-verified',
|
||||
ID_CARD_EXISTS: 'uni-id-idcard-exists',
|
||||
INVALID_ID_CARD: 'uni-id-invalid-idcard',
|
||||
INVALID_REAL_NAME: 'uni-id-invalid-realname',
|
||||
UNKNOWN_ERROR: 'uni-id-unknown-error',
|
||||
REAL_NAME_VERIFY_UPPER_LIMIT: 'uni-id-realname-verify-upper-limit'
|
||||
}
|
||||
|
||||
function isUniIdError (errCode) {
|
||||
return Object.values(ERROR).includes(errCode)
|
||||
}
|
||||
|
||||
class UniCloudError extends Error {
|
||||
constructor (options) {
|
||||
super(options.message)
|
||||
this.errMsg = options.message || ''
|
||||
this.errCode = options.code
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
ERROR,
|
||||
isUniIdError,
|
||||
UniCloudError
|
||||
}
|
@ -0,0 +1,64 @@
|
||||
const crypto = require('crypto')
|
||||
const { ERROR } = require('./error')
|
||||
|
||||
function checkSecret (secret) {
|
||||
if (!secret) {
|
||||
throw {
|
||||
errCode: ERROR.CONFIG_FIELD_REQUIRED,
|
||||
errMsgValue: {
|
||||
field: 'sensitiveInfoEncryptSecret'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (secret.length !== 32) {
|
||||
throw {
|
||||
errCode: ERROR.CONFIG_FIELD_INVALID,
|
||||
errMsgValue: {
|
||||
field: 'sensitiveInfoEncryptSecret'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
function encryptData (text = '') {
|
||||
if (!text) return text
|
||||
|
||||
const encryptSecret = this.config.sensitiveInfoEncryptSecret
|
||||
|
||||
checkSecret(encryptSecret)
|
||||
|
||||
const iv = encryptSecret.slice(-16)
|
||||
|
||||
const cipher = crypto.createCipheriv('aes-256-cbc', encryptSecret, iv)
|
||||
|
||||
const encrypted = Buffer.concat([
|
||||
cipher.update(Buffer.from(text, 'utf-8')),
|
||||
cipher.final()
|
||||
])
|
||||
|
||||
return encrypted.toString('base64')
|
||||
}
|
||||
|
||||
function decryptData (text = '') {
|
||||
if (!text) return text
|
||||
|
||||
const encryptSecret = this.config.sensitiveInfoEncryptSecret
|
||||
|
||||
checkSecret(encryptSecret)
|
||||
|
||||
const iv = encryptSecret.slice(-16)
|
||||
|
||||
const cipher = crypto.createDecipheriv('aes-256-cbc', encryptSecret, iv)
|
||||
|
||||
const decrypted = Buffer.concat([
|
||||
cipher.update(Buffer.from(text, 'base64')),
|
||||
cipher.final()
|
||||
])
|
||||
|
||||
return decrypted.toString('utf-8')
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
encryptData,
|
||||
decryptData
|
||||
}
|
@ -0,0 +1,47 @@
|
||||
const { ERROR } = require('./error')
|
||||
|
||||
function getHttpClientInfo () {
|
||||
const requestId = this.getUniCloudRequestId()
|
||||
const { clientIP, userAgent, source, secretType = 'none' } = this.getClientInfo()
|
||||
const { clientInfo = {} } = JSON.parse(this.getHttpInfo().body)
|
||||
|
||||
return {
|
||||
...clientInfo,
|
||||
clientIP,
|
||||
userAgent,
|
||||
source,
|
||||
secretType,
|
||||
requestId
|
||||
}
|
||||
}
|
||||
|
||||
function getHttpUniIdToken () {
|
||||
const { uniIdToken = '' } = JSON.parse(this.getHttpInfo().body)
|
||||
|
||||
return uniIdToken
|
||||
}
|
||||
|
||||
function verifyHttpMethod () {
|
||||
const { headers, httpMethod } = this.getHttpInfo()
|
||||
|
||||
if (!/^application\/json/.test(headers['content-type']) || httpMethod.toUpperCase() !== 'POST') {
|
||||
throw {
|
||||
errCode: ERROR.UNSUPPORTED_REQUEST,
|
||||
errMsg: 'unsupported request'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function universal () {
|
||||
if (this.getClientInfo().source === 'http') {
|
||||
verifyHttpMethod.call(this)
|
||||
this.getParams()[0] = JSON.parse(this.getHttpInfo().body).params
|
||||
this.getUniversalClientInfo = getHttpClientInfo.bind(this)
|
||||
this.getUniversalUniIdToken = getHttpUniIdToken.bind(this)
|
||||
} else {
|
||||
this.getUniversalClientInfo = this.getClientInfo
|
||||
this.getUniversalUniIdToken = this.getUniIdToken
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = universal
|
@ -0,0 +1,263 @@
|
||||
function batchFindObjctValue (obj = {}, keys = []) {
|
||||
const values = {}
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
const key = keys[i]
|
||||
const keyPath = key.split('.')
|
||||
let currentKey = keyPath.shift()
|
||||
let result = obj
|
||||
while (currentKey) {
|
||||
if (!result) {
|
||||
break
|
||||
}
|
||||
result = result[currentKey]
|
||||
currentKey = keyPath.shift()
|
||||
}
|
||||
values[key] = result
|
||||
}
|
||||
return values
|
||||
}
|
||||
|
||||
function getType (val) {
|
||||
return Object.prototype.toString.call(val).slice(8, -1).toLowerCase()
|
||||
}
|
||||
|
||||
function hasOwn (obj, key) {
|
||||
return Object.prototype.hasOwnProperty.call(obj, key)
|
||||
}
|
||||
|
||||
function isValidString (val) {
|
||||
return val && getType(val) === 'string'
|
||||
}
|
||||
|
||||
function isPlainObject (obj) {
|
||||
return getType(obj) === 'object'
|
||||
}
|
||||
|
||||
function isFn (fn) {
|
||||
// 务必注意AsyncFunction
|
||||
return typeof fn === 'function'
|
||||
}
|
||||
|
||||
// 获取文件后缀,只添加几种图片类型供客服消息接口使用
|
||||
const mime2ext = {
|
||||
'image/png': 'png',
|
||||
'image/jpeg': 'jpg',
|
||||
'image/gif': 'gif',
|
||||
'image/svg+xml': 'svg',
|
||||
'image/bmp': 'bmp',
|
||||
'image/webp': 'webp'
|
||||
}
|
||||
|
||||
function getExtension (contentType) {
|
||||
return mime2ext[contentType]
|
||||
}
|
||||
|
||||
const isSnakeCase = /_(\w)/g
|
||||
const isCamelCase = /[A-Z]/g
|
||||
|
||||
function snake2camel (value) {
|
||||
return value.replace(isSnakeCase, (_, c) => (c ? c.toUpperCase() : ''))
|
||||
}
|
||||
|
||||
function camel2snake (value) {
|
||||
return value.replace(isCamelCase, str => '_' + str.toLowerCase())
|
||||
}
|
||||
|
||||
function parseObjectKeys (obj, type) {
|
||||
let parserReg, parser
|
||||
switch (type) {
|
||||
case 'snake2camel':
|
||||
parser = snake2camel
|
||||
parserReg = isSnakeCase
|
||||
break
|
||||
case 'camel2snake':
|
||||
parser = camel2snake
|
||||
parserReg = isCamelCase
|
||||
break
|
||||
}
|
||||
for (const key in obj) {
|
||||
if (hasOwn(obj, key)) {
|
||||
if (parserReg.test(key)) {
|
||||
const keyCopy = parser(key)
|
||||
obj[keyCopy] = obj[key]
|
||||
delete obj[key]
|
||||
if (isPlainObject(obj[keyCopy])) {
|
||||
obj[keyCopy] = parseObjectKeys(obj[keyCopy], type)
|
||||
} else if (Array.isArray(obj[keyCopy])) {
|
||||
obj[keyCopy] = obj[keyCopy].map((item) => {
|
||||
return parseObjectKeys(item, type)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return obj
|
||||
}
|
||||
|
||||
function snake2camelJson (obj) {
|
||||
return parseObjectKeys(obj, 'snake2camel')
|
||||
}
|
||||
|
||||
function camel2snakeJson (obj) {
|
||||
return parseObjectKeys(obj, 'camel2snake')
|
||||
}
|
||||
|
||||
function getOffsetDate (offset) {
|
||||
return new Date(
|
||||
Date.now() + (new Date().getTimezoneOffset() + (offset || 0) * 60) * 60000
|
||||
)
|
||||
}
|
||||
|
||||
function getDateStr (date, separator = '-') {
|
||||
date = date || new Date()
|
||||
const dateArr = []
|
||||
dateArr.push(date.getFullYear())
|
||||
dateArr.push(('00' + (date.getMonth() + 1)).substr(-2))
|
||||
dateArr.push(('00' + date.getDate()).substr(-2))
|
||||
return dateArr.join(separator)
|
||||
}
|
||||
|
||||
function getTimeStr (date, separator = ':') {
|
||||
date = date || new Date()
|
||||
const timeArr = []
|
||||
timeArr.push(('00' + date.getHours()).substr(-2))
|
||||
timeArr.push(('00' + date.getMinutes()).substr(-2))
|
||||
timeArr.push(('00' + date.getSeconds()).substr(-2))
|
||||
return timeArr.join(separator)
|
||||
}
|
||||
|
||||
function getFullTimeStr (date) {
|
||||
date = date || new Date()
|
||||
return getDateStr(date) + ' ' + getTimeStr(date)
|
||||
}
|
||||
|
||||
function getDistinctArray (arr) {
|
||||
return Array.from(new Set(arr))
|
||||
}
|
||||
|
||||
/**
|
||||
* 拼接url
|
||||
* @param {string} base 基础路径
|
||||
* @param {string} path 在基础路径上拼接的路径
|
||||
* @returns
|
||||
*/
|
||||
function resolveUrl (base, path) {
|
||||
if (/^https?:/.test(path)) {
|
||||
return path
|
||||
}
|
||||
return base + path
|
||||
}
|
||||
|
||||
function getVerifyCode (len = 6) {
|
||||
let code = ''
|
||||
for (let i = 0; i < len; i++) {
|
||||
code += Math.floor(Math.random() * 10)
|
||||
}
|
||||
return code
|
||||
}
|
||||
|
||||
function coverMobile (mobile) {
|
||||
if (typeof mobile !== 'string') {
|
||||
return mobile
|
||||
}
|
||||
return mobile.slice(0, 3) + '****' + mobile.slice(7)
|
||||
}
|
||||
|
||||
function getNonceStr (length = 16) {
|
||||
let str = ''
|
||||
while (str.length < length) {
|
||||
str += Math.random().toString(32).substring(2)
|
||||
}
|
||||
return str.substring(0, length)
|
||||
}
|
||||
|
||||
function isMatchUserApp (userAppList, matchAppList) {
|
||||
if (userAppList === undefined || userAppList === null) {
|
||||
return true
|
||||
}
|
||||
if (getType(userAppList) !== 'array') {
|
||||
return false
|
||||
}
|
||||
if (userAppList.includes('*')) {
|
||||
return true
|
||||
}
|
||||
if (getType(matchAppList) === 'string') {
|
||||
matchAppList = [matchAppList]
|
||||
}
|
||||
return userAppList.some(item => matchAppList.includes(item))
|
||||
}
|
||||
|
||||
function checkIdCard (idCardNumber) {
|
||||
if (!idCardNumber || typeof idCardNumber !== 'string' || idCardNumber.length !== 18) return false
|
||||
|
||||
const coefficient = [7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2]
|
||||
const checkCode = [1, 0, 'x', 9, 8, 7, 6, 5, 4, 3, 2]
|
||||
const code = idCardNumber.substring(17)
|
||||
|
||||
let sum = 0
|
||||
for (let i = 0; i < 17; i++) {
|
||||
sum += Number(idCardNumber.charAt(i)) * coefficient[i]
|
||||
}
|
||||
|
||||
return checkCode[sum % 11].toString() === code.toLowerCase()
|
||||
}
|
||||
|
||||
function catchAwait (fn, finallyFn) {
|
||||
if (!fn) return [new Error('no function')]
|
||||
|
||||
if (Promise.prototype.finally === undefined) {
|
||||
// eslint-disable-next-line no-extend-native
|
||||
Promise.prototype.finally = function (finallyFn) {
|
||||
return this.then(
|
||||
res => Promise.resolve(finallyFn()).then(() => res),
|
||||
error => Promise.resolve(finallyFn()).then(() => { throw error })
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return fn
|
||||
.then((data) => [undefined, data])
|
||||
.catch((error) => [error])
|
||||
.finally(() => typeof finallyFn === 'function' && finallyFn())
|
||||
}
|
||||
|
||||
function dataDesensitization (value = '', options = {}) {
|
||||
const { onlyLast = false } = options
|
||||
const [firstIndex, middleIndex, lastIndex] = onlyLast ? [0, 0, -1] : [0, 1, -1]
|
||||
|
||||
if (!value) return value
|
||||
const first = value.slice(firstIndex, middleIndex)
|
||||
const middle = value.slice(middleIndex, lastIndex)
|
||||
const last = value.slice(lastIndex)
|
||||
const star = Array.from(new Array(middle.length), (v) => '*').join('')
|
||||
|
||||
return first + star + last
|
||||
}
|
||||
|
||||
function getCurrentDateTimestamp (date = Date.now(), targetTimezone = 8) {
|
||||
const oneHour = 60 * 60 * 1000
|
||||
return parseInt((date + targetTimezone * oneHour) / (24 * oneHour)) * (24 * oneHour) - targetTimezone * oneHour
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getType,
|
||||
isValidString,
|
||||
batchFindObjctValue,
|
||||
isPlainObject,
|
||||
isFn,
|
||||
getDistinctArray,
|
||||
getFullTimeStr,
|
||||
resolveUrl,
|
||||
getOffsetDate,
|
||||
camel2snakeJson,
|
||||
snake2camelJson,
|
||||
getExtension,
|
||||
getVerifyCode,
|
||||
coverMobile,
|
||||
getNonceStr,
|
||||
isMatchUserApp,
|
||||
checkIdCard,
|
||||
catchAwait,
|
||||
dataDesensitization,
|
||||
getCurrentDateTimestamp
|
||||
}
|
@ -0,0 +1,443 @@
|
||||
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
|
||||
}
|
Reference in New Issue
Block a user