首次完整推送,

V:1.20240808.006
This commit is contained in:
fm453
2024-08-13 18:32:37 +08:00
parent 15be3e9373
commit c62d15b288
939 changed files with 111777 additions and 0 deletions

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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

View File

@ -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
}

View File

@ -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
}

View File

@ -0,0 +1,90 @@
// 各接口权限配置,未配置接口表示允许任何用户访问(包括未登录用户)
module.exports = {
// 管理接口
addUser: {
// auth: true // 已登录用户方可操作,配置角色或权限时此项可不写
role: ['admin'] // 允许进行此操作的角色,包含任一角色均可操作。
// permission: [] // 允许进行此操作的权限,包含任一权限均可操作。
// 权限角色均配置时,用户拥有任一权限或任一角色均可操作
},
updateUser: {
role: ['admin']
},
authorizeAppLogin: {
role: ['admin']
},
removeAuthorizedApp: {
role: ['admin']
},
setAuthorizedApp: {
role: ['admin']
},
// 用户接口
closeAccount: {
auth: true
},
updatePwd: {
auth: true
},
logout: {
auth: true
},
bindMobileBySms: {
auth: true
},
bindMobileByUniverify: {
auth: true
},
bindMobileByMpWeixin: {
auth: true
},
bindAlipay: {
auth: true
},
bindApple: {
auth: true
},
bindQQ: {
auth: true
},
bindWeixin: {
auth: true
},
acceptInvite: {
auth: true
},
getInvitedUser: {
auth: true
},
setPushCid: {
auth: true
},
getAccountInfo: {
auth: true
},
unbindWeixin: {
auth: true
},
unbindAlipay: {
auth: true
},
unbindQQ: {
auth: true
},
unbindApple: {
auth: true
},
setPwd: {
auth: true
},
getFrvCertifyId: {
auth: true
},
getFrvAuthResult: {
auth: true
},
getRealNameInfo: {
auth: true
}
}

View File

@ -0,0 +1,696 @@
const uniIdCommon = require('uni-id-common')
const uniCaptcha = require('uni-captcha')
const {
getType,
checkIdCard
} = require('./common/utils')
const {
checkClientInfo,
Validator
} = require('./common/validator')
const ConfigUtils = require('./lib/utils/config')
const {
isUniIdError,
ERROR
} = require('./common/error')
const middleware = require('./middleware/index')
const universal = require('./common/universal')
const {
registerAdmin,
registerUser,
registerUserByEmail
} = require('./module/register/index')
const {
addUser,
updateUser
} = require('./module/admin/index')
const {
login,
loginBySms,
loginByUniverify,
loginByWeixin,
loginByAlipay,
loginByQQ,
loginByApple,
loginByWeixinMobile
} = require('./module/login/index')
const {
logout
} = require('./module/logout/index')
const {
bindMobileBySms,
bindMobileByUniverify,
bindMobileByMpWeixin,
bindAlipay,
bindApple,
bindQQ,
bindWeixin,
unbindWeixin,
unbindAlipay,
unbindQQ,
unbindApple
} = require('./module/relate/index')
const {
setPwd,
updatePwd,
resetPwdBySms,
resetPwdByEmail,
closeAccount,
getAccountInfo,
getRealNameInfo
} = require('./module/account/index')
const {
createCaptcha,
refreshCaptcha,
sendSmsCode,
sendEmailCode
} = require('./module/verify/index')
const {
refreshToken,
setPushCid,
secureNetworkHandshakeByWeixin
} = require('./module/utils/index')
const {
getInvitedUser,
acceptInvite
} = require('./module/fission')
const {
authorizeAppLogin,
removeAuthorizedApp,
setAuthorizedApp
} = require('./module/multi-end')
const {
getSupportedLoginType
} = require('./module/dev/index')
const {
externalRegister,
externalLogin,
updateUserInfoByExternal
} = require('./module/external')
const {
getFrvCertifyId,
getFrvAuthResult
} = require('./module/facial-recognition-verify')
module.exports = {
async _before () {
// 支持 callFunction 与 URL化
universal.call(this)
const clientInfo = this.getUniversalClientInfo()
/**
* 检查clientInfo无appId和uniPlatform时本云对象无法正常运行
* 此外需要保证用到的clientInfo字段均经过类型检查
* clientInfo由客户端上传并非完全可信clientInfo内除clientIP、userAgent、source外均为客户端上传参数
* 否则可能会出现一些意料外的情况
*/
checkClientInfo(clientInfo)
let clientPlatform = clientInfo.uniPlatform
// 统一platform名称
switch (clientPlatform) {
case 'app':
case 'app-plus':
case 'app-android':
case 'app-ios':
clientPlatform = 'app'
break
case 'web':
case 'h5':
clientPlatform = 'web'
break
default:
break
}
this.clientPlatform = clientPlatform
// 挂载uni-id实例到this上方便后续调用
this.uniIdCommon = uniIdCommon.createInstance({
clientInfo
})
// 包含uni-id配置合并等功能的工具集
this.configUtils = new ConfigUtils({
context: this
})
this.config = this.configUtils.getPlatformConfig()
this.hooks = this.configUtils.getHooks()
this.validator = new Validator({
passwordStrength: this.config.passwordStrength
})
// 扩展 validator 增加 验证身份证号码合法性
this.validator.mixin('idCard', function (idCard) {
if (!checkIdCard(idCard)) {
return {
errCode: ERROR.INVALID_ID_CARD
}
}
})
this.validator.mixin('realName', function (realName) {
if (
typeof realName !== 'string' ||
realName.length < 2 ||
!/^[\u4e00-\u9fa5]{1,10}(·?[\u4e00-\u9fa5]{1,10}){0,5}$/.test(realName)
) {
return {
errCode: ERROR.INVALID_REAL_NAME
}
}
})
/**
* 示例:覆盖密码验证规则
*/
// this.validator.mixin('password', function (password) {
// if (typeof password !== 'string' || password.length < 10) {
// // 调整为密码长度不能小于10
// return {
// errCode: ERROR.INVALID_PASSWORD
// }
// }
// })
/**
* 示例:新增验证规则
*/
// this.validator.mixin('timestamp', function (timestamp) {
// if (typeof timestamp !== 'number' || timestamp > Date.now()) {
// return {
// errCode: ERROR.INVALID_PARAM
// }
// }
// })
// // 新增规则同样可以在数组验证规则中使用
// this.validator.validate({
// timestamp: 123456789
// }, {
// timestamp: 'timestamp'
// })
// this.validator.validate({
// timestampList: [123456789, 123123123123]
// }, {
// timestampList: 'array<timestamp>'
// })
// // 甚至更复杂的写法
// this.validator.validate({
// timestamp: [123456789123123123, 123123123123]
// }, {
// timestamp: 'timestamp|array<timestamp|number>'
// })
// 挂载uni-captcha到this上方便后续调用
this.uniCaptcha = uniCaptcha
Object.defineProperty(this, 'uniOpenBridge', {
get () {
return require('uni-open-bridge-common')
}
})
// 挂载中间件
this.middleware = {}
for (const mwName in middleware) {
this.middleware[mwName] = middleware[mwName].bind(this)
}
// 国际化
const messages = require('./lang/index')
const fallbackLocale = 'zh-Hans'
const i18n = uniCloud.initI18n({
locale: clientInfo.locale,
fallbackLocale,
messages: JSON.parse(JSON.stringify(messages))
})
if (!messages[i18n.locale]) {
i18n.setLocale(fallbackLocale)
}
this.t = i18n.t.bind(i18n)
this.response = {}
// 请求鉴权验证
await this.middleware.verifyRequestSign()
// 通用权限校验模块
await this.middleware.accessControl()
},
_after (error, result) {
if (error) {
// 处理中间件内抛出的标准响应对象
if (error.errCode && getType(error) === 'object') {
const errCode = error.errCode
if (!isUniIdError(errCode)) {
return error
}
return {
errCode,
errMsg: error.errMsg || this.t(errCode, error.errMsgValue)
}
}
throw error
}
return Object.assign(this.response, result)
},
/**
* 注册管理员
* @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#register-admin
* @param {Object} params
* @param {String} params.username 用户名
* @param {String} params.password 密码
* @param {String} params.nickname 昵称
* @returns
*/
registerAdmin,
/**
* 新增用户
* @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#add-user
* @param {Object} params
* @param {String} params.username 用户名
* @param {String} params.password 密码
* @param {String} params.nickname 昵称
* @param {Array} params.authorizedApp 允许登录的AppID列表
* @param {Array} params.role 用户角色列表
* @param {String} params.mobile 手机号
* @param {String} params.email 邮箱
* @param {Array} params.tags 用户标签
* @param {Number} params.status 用户状态
* @returns
*/
addUser,
/**
* 修改用户
* @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#update-user
* @param {Object} params
* @param {String} params.id 要更新的用户id
* @param {String} params.username 用户名
* @param {String} params.password 密码
* @param {String} params.nickname 昵称
* @param {Array} params.authorizedApp 允许登录的AppID列表
* @param {Array} params.role 用户角色列表
* @param {String} params.mobile 手机号
* @param {String} params.email 邮箱
* @param {Array} params.tags 用户标签
* @param {Number} params.status 用户状态
* @returns
*/
updateUser,
/**
* 授权用户登录应用
* @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#authorize-app-login
* @param {Object} params
* @param {String} params.uid 用户id
* @param {String} params.appId 授权的应用的AppId
* @returns
*/
authorizeAppLogin,
/**
* 移除用户登录授权
* @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#remove-authorized-app
* @param {Object} params
* @param {String} params.uid 用户id
* @param {String} params.appId 取消授权的应用的AppId
* @returns
*/
removeAuthorizedApp,
/**
* 设置用户允许登录的应用列表
* @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#set-authorized-app
* @param {Object} params
* @param {String} params.uid 用户id
* @param {Array} params.appIdList 允许登录的应用AppId列表
* @returns
*/
setAuthorizedApp,
/**
* 注册普通用户
* @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#register-user
* @param {Object} params
* @param {String} params.username 用户名
* @param {String} params.password 密码
* @param {String} params.captcha 图形验证码
* @param {String} params.nickname 昵称
* @param {String} params.inviteCode 邀请码
* @returns
*/
registerUser,
/**
* 通过邮箱+验证码注册用户
* @param {Object} params
* @param {String} params.email 邮箱
* @param {String} params.password 密码
* @param {String} params.nickname 昵称
* @param {String} params.code 邮箱验证码
* @param {String} params.inviteCode 邀请码
* @returns
*/
registerUserByEmail,
/**
* 用户名密码登录
* @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#login
* @param {Object} params
* @param {String} params.username 用户名
* @param {String} params.mobile 手机号
* @param {String} params.email 邮箱
* @param {String} params.password 密码
* @param {String} params.captcha 图形验证码
* @returns
*/
login,
/**
* 短信验证码登录
* @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#login-by-sms
* @param {Object} params
* @param {String} params.mobile 手机号
* @param {String} params.code 短信验证码
* @param {String} params.captcha 图形验证码
* @param {String} params.inviteCode 邀请码
* @returns
*/
loginBySms,
/**
* App端一键登录
* @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#login-by-univerify
* @param {Object} params
* @param {String} params.access_token APP端一键登录返回的access_token
* @param {String} params.openid APP端一键登录返回的openid
* @param {String} params.inviteCode 邀请码
* @returns
*/
loginByUniverify,
/**
* 微信登录
* @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#login-by-weixin
* @param {Object} params
* @param {String} params.code 微信登录返回的code
* @param {String} params.inviteCode 邀请码
* @returns
*/
loginByWeixin,
/**
* 支付宝登录
* @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#login-by-alipay
* @param {Object} params
* @param {String} params.code 支付宝小程序客户端登录返回的code
* @param {String} params.inviteCode 邀请码
* @returns
*/
loginByAlipay,
/**
* QQ登录
* @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#login-by-qq
* @param {Object} params
* @param {String} params.code QQ小程序登录返回的code参数
* @param {String} params.accessToken App端QQ登录返回的accessToken参数
* @param {String} params.accessTokenExpired accessToken过期时间由App端QQ登录返回的expires_in参数计算而来单位毫秒
* @param {String} params.inviteCode 邀请码
* @returns
*/
loginByQQ,
/**
* 苹果登录
* @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#login-by-apple
* @param {Object} params
* @param {String} params.identityToken 苹果登录返回的identityToken
* @param {String} params.nickname 用户昵称
* @param {String} params.inviteCode 邀请码
* @returns
*/
loginByApple,
loginByWeixinMobile,
/**
* 用户退出登录
* @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#logout
* @returns
*/
logout,
/**
* 通过短信验证码绑定手机号
* @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#bind-mobile-by-sms
* @param {Object} params
* @param {String} params.mobile 手机号
* @param {String} params.code 短信验证码
* @param {String} params.captcha 图形验证码
* @returns
*/
bindMobileBySms,
/**
* 通过一键登录绑定手机号
* @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#bind-mobile-by-univerify
* @param {Object} params
* @param {String} params.openid APP端一键登录返回的openid
* @param {String} params.access_token APP端一键登录返回的access_token
* @returns
*/
bindMobileByUniverify,
/**
* 通过微信绑定手机号
* @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#bind-mobile-by-mp-weixin
* @param {Object} params
* @param {String} params.encryptedData 微信获取手机号返回的加密信息
* @param {String} params.iv 微信获取手机号返回的初始向量
* @returns
*/
bindMobileByMpWeixin,
/**
* 绑定微信
* @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#bind-weixin
* @param {Object} params
* @param {String} params.code 微信登录返回的code
* @returns
*/
bindWeixin,
/**
* 绑定QQ
* @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#bind-qq
* @param {Object} params
* @param {String} params.code 小程序端QQ登录返回的code
* @param {String} params.accessToken APP端QQ登录返回的accessToken
* @param {String} params.accessTokenExpired accessToken过期时间由App端QQ登录返回的expires_in参数计算而来单位毫秒
* @returns
*/
bindQQ,
/**
* 绑定支付宝账号
* @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#bind-alipay
* @param {Object} params
* @param {String} params.code 支付宝小程序登录返回的code参数
* @returns
*/
bindAlipay,
/**
* 绑定苹果账号
* @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#bind-apple
* @param {Object} params
* @param {String} params.identityToken 苹果登录返回identityToken
* @returns
*/
bindApple,
/**
* 更新密码
* @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#update-pwd
* @param {object} params
* @param {string} params.oldPassword 旧密码
* @param {string} params.newPassword 新密码
* @returns {object}
*/
updatePwd,
/**
* 通过短信验证码重置密码
* @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#reset-pwd-by-sms
* @param {object} params
* @param {string} params.mobile 手机号
* @param {string} params.mobile 短信验证码
* @param {string} params.password 密码
* @param {string} params.captcha 图形验证码
* @returns {object}
*/
resetPwdBySms,
/**
* 通过邮箱验证码重置密码
* @param {object} params
* @param {string} params.email 邮箱
* @param {string} params.code 邮箱验证码
* @param {string} params.password 密码
* @param {string} params.captcha 图形验证码
* @returns {object}
*/
resetPwdByEmail,
/**
* 注销账户
* @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#close-account
* @returns
*/
closeAccount,
/**
* 获取账户账户简略信息
* @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#get-account-info
*/
getAccountInfo,
/**
* 创建图形验证码
* @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#create-captcha
* @param {Object} params
* @param {String} params.scene 图形验证码使用场景
* @returns
*/
createCaptcha,
/**
* 刷新图形验证码
* @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#refresh-captcha
* @param {Object} params
* @param {String} params.scene 图形验证码使用场景
* @returns
*/
refreshCaptcha,
/**
* 发送短信验证码
* @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#send-sms-code
* @param {Object} params
* @param {String} params.mobile 手机号
* @param {String} params.captcha 图形验证码
* @param {String} params.scene 短信验证码使用场景
* @returns
*/
sendSmsCode,
/**
* 发送邮箱验证码
* @tutorial 需自行实现功能
* @param {Object} params
* @param {String} params.email 邮箱
* @param {String} params.captcha 图形验证码
* @param {String} params.scene 短信验证码使用场景
* @returns
*/
sendEmailCode,
/**
* 刷新token
* @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#refresh-token
*/
refreshToken,
/**
* 接受邀请
* @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#accept-invite
* @param {Object} params
* @param {String} params.inviteCode 邀请码
* @returns
*/
acceptInvite,
/**
* 获取受邀用户
* @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#get-invited-user
* @param {Object} params
* @param {Number} params.level 获取受邀用户的级数1表示直接邀请的用户
* @param {Number} params.limit 返回数据大小
* @param {Number} params.offset 返回数据偏移
* @param {Boolean} params.needTotal 是否需要返回总数
* @returns
*/
getInvitedUser,
/**
* 更新device表的push_clien_id
* @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#set-push-cid
* @param {object} params
* @param {string} params.pushClientId 客户端pushClientId
* @returns
*/
setPushCid,
/**
* 获取支持的登录方式
* @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#get-supported-login-type
* @returns
*/
getSupportedLoginType,
/**
* 解绑微信
* @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#unbind-weixin
* @returns
*/
unbindWeixin,
/**
* 解绑支付宝
* @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#unbind-alipay
* @returns
*/
unbindAlipay,
/**
* 解绑QQ
* @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#unbind-qq
* @returns
*/
unbindQQ,
/**
* 解绑Apple
* @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#unbind-apple
* @returns
*/
unbindApple,
/**
* 安全网络握手,目前仅处理微信小程序安全网络握手
*/
secureNetworkHandshakeByWeixin,
/**
* 设置密码
* @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#set-pwd
* @returns
*/
setPwd,
/**
* 外部注册用户
* @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#external-register
* @param {object} params
* @param {string} params.externalUid 业务系统的用户id
* @param {string} params.nickname 昵称
* @param {string} params.gender 性别
* @param {string} params.avatar 头像
* @returns {object}
*/
externalRegister,
/**
* 外部用户登录
* @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#external-login
* @param {object} params
* @param {string} params.userId uni-id体系用户id
* @param {string} params.externalUid 业务系统的用户id
* @returns {object}
*/
externalLogin,
/**
* 使用 userId 或 externalUid 获取用户信息
* @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#external-update-userinfo
* @param {object} params
* @param {string} params.userId uni-id体系的用户id
* @param {string} params.externalUid 业务系统的用户id
* @param {string} params.nickname 昵称
* @param {string} params.gender 性别
* @param {string} params.avatar 头像
* @returns {object}
*/
updateUserInfoByExternal,
/**
* 获取认证ID
* @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#get-frv-certify-id
* @param {Object} params
* @param {String} params.realName 真实姓名
* @param {String} params.idCard 身份证号码
* @returns
*/
getFrvCertifyId,
/**
* 查询认证结果
* @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#get-frv-auth-result
* @param {Object} params
* @param {String} params.certifyId 认证ID
* @param {String} params.needAlivePhoto 是否获取认证照片Y_O 原始图片、Y_M虚化背景马赛克、N不返图
* @returns
*/
getFrvAuthResult,
/**
* 获取实名信息
* @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#get-realname-info
* @param {Object} params
* @param {Boolean} params.decryptData 是否解密数据
* @returns
*/
getRealNameInfo
}

View File

@ -0,0 +1,62 @@
const word = {
login: 'login',
'verify-mobile': 'verify phone number'
}
const sentence = {
'uni-id-account-exists': 'Account exists',
'uni-id-account-not-exists': 'Account does not exists',
'uni-id-account-not-exists-in-current-app': 'Account does not exists in current app',
'uni-id-account-conflict': 'User account conflict',
'uni-id-account-banned': 'Account has been banned',
'uni-id-account-auditing': 'Account audit in progress',
'uni-id-account-audit-failed': 'Account audit failed',
'uni-id-account-closed': 'Account has been closed',
'uni-id-captcha-required': 'Captcha required',
'uni-id-password-error': 'Password error',
'uni-id-password-error-exceed-limit': 'The number of password errors is excessive',
'uni-id-invalid-username': 'Invalid username',
'uni-id-invalid-password': 'invalid password',
'uni-id-invalid-password-super': 'Passwords must have 8-16 characters and contain uppercase letters, lowercase letters, numbers, and symbols.',
'uni-id-invalid-password-strong': 'Passwords must have 8-16 characters and contain letters, numbers and symbols.',
'uni-id-invalid-password-medium': 'Passwords must have 8-16 characters and contain at least two of the following: letters, numbers, and symbols.',
'uni-id-invalid-password-weak': 'Passwords must have 6-16 characters and contain letters and numbers.',
'uni-id-invalid-mobile': 'Invalid mobile phone number',
'uni-id-invalid-email': 'Invalid email address',
'uni-id-invalid-nickname': 'Invalid nickname',
'uni-id-invalid-param': 'Invalid parameter',
'uni-id-param-required': 'Parameter required: {param}',
'uni-id-get-third-party-account-failed': 'Get third party account failed',
'uni-id-get-third-party-user-info-failed': 'Get third party user info failed',
'uni-id-mobile-verify-code-error': 'Verify code error or expired',
'uni-id-email-verify-code-error': 'Verify code error or expired',
'uni-id-admin-exists': 'Administrator exists',
'uni-id-permission-error': 'Permission denied',
'uni-id-system-error': 'System error',
'uni-id-set-invite-code-failed': 'Set invite code failed',
'uni-id-invalid-invite-code': 'Invalid invite code',
'uni-id-change-inviter-forbidden': 'Change inviter is not allowed',
'uni-id-bind-conflict': 'This account has been bound',
'uni-id-admin-exist-in-other-apps': 'Administrator is registered in other consoles',
'uni-id-unbind-failed': 'Please bind first and then unbind',
'uni-id-unbind-not-supported': 'Unbinding is not supported',
'uni-id-unbind-mobile-not-exists': 'This is the only way to login at the moment, please bind your phone number and then try to unbind',
'uni-id-unbind-password-not-exists': 'Please set a password first',
'uni-id-unsupported-request': 'Unsupported request',
'uni-id-illegal-request': 'Illegal request',
'uni-id-config-field-required': 'Config field required: {field}',
'uni-id-config-field-invalid': 'Config field: {field} is invalid',
'uni-id-frv-fail': 'Real name certify failed',
'uni-id-frv-processing': 'Waiting for face recognition',
'uni-id-realname-verified': 'This account has been verified',
'uni-id-idcard-exists': 'The ID number has been bound to the account',
'uni-id-invalid-idcard': 'ID number is invalid',
'uni-id-invalid-realname': 'The name can only be Chinese characters',
'uni-id-unknown-error': 'unknown error',
'uni-id-realname-verify-upper-limit': 'The number of real-name certify on the day has reached the upper limit'
}
module.exports = {
...word,
...sentence
}

View File

@ -0,0 +1,22 @@
let lang = {
'zh-Hans': require('./zh-hans'),
en: require('./en')
}
function mergeLanguage (lang1, lang2) {
const localeList = Object.keys(lang1)
localeList.push(...Object.keys(lang2))
const result = {}
for (let i = 0; i < localeList.length; i++) {
const locale = localeList[i]
result[locale] = Object.assign({}, lang1[locale], lang2[locale])
}
return result
}
try {
const langPath = require.resolve('uni-config-center/uni-id/lang/index.js')
lang = mergeLanguage(lang, require(langPath))
} catch (error) { }
module.exports = lang

View File

@ -0,0 +1,64 @@
const word = {
login: '登录',
'verify-mobile': '验证手机号'
}
const sentence = {
'uni-id-token-expired': '登录状态失效token已过期',
'uni-id-check-token-failed': 'token校验未通过',
'uni-id-account-exists': '此账号已注册',
'uni-id-account-not-exists': '此账号未注册',
'uni-id-account-not-exists-in-current-app': '此账号未在该应用注册',
'uni-id-account-conflict': '用户账号冲突',
'uni-id-account-banned': '此账号已封禁',
'uni-id-account-auditing': '此账号正在审核中',
'uni-id-account-audit-failed': '此账号审核失败',
'uni-id-account-closed': '此账号已注销',
'uni-id-captcha-required': '请输入图形验证码',
'uni-id-password-error': '密码错误',
'uni-id-password-error-exceed-limit': '密码错误次数过多,请稍后再试',
'uni-id-invalid-username': '用户名不合法',
'uni-id-invalid-password': '密码不合法',
'uni-id-invalid-password-super': '密码必须包含大小写字母、数字和特殊符号长度8-16位',
'uni-id-invalid-password-strong': '密码必须包含字母、数字和特殊符号长度8-16位不合法',
'uni-id-invalid-password-medium': '密码必须为字母、数字和特殊符号任意两种的组合长度8-16位',
'uni-id-invalid-password-weak': '密码必须包含字母和数字长度6-16位',
'uni-id-invalid-mobile': '手机号码不合法',
'uni-id-invalid-email': '邮箱不合法',
'uni-id-invalid-nickname': '昵称不合法',
'uni-id-invalid-param': '参数错误',
'uni-id-param-required': '缺少参数: {param}',
'uni-id-get-third-party-account-failed': '获取第三方账号失败',
'uni-id-get-third-party-user-info-failed': '获取用户信息失败',
'uni-id-mobile-verify-code-error': '手机验证码错误或已过期',
'uni-id-email-verify-code-error': '邮箱验证码错误或已过期',
'uni-id-admin-exists': '超级管理员已存在',
'uni-id-permission-error': '权限错误',
'uni-id-system-error': '系统错误',
'uni-id-set-invite-code-failed': '设置邀请码失败',
'uni-id-invalid-invite-code': '邀请码不可用',
'uni-id-change-inviter-forbidden': '禁止修改邀请人',
'uni-id-bind-conflict': '此账号已被绑定',
'uni-id-admin-exist-in-other-apps': '超级管理员已在其他控制台注册',
'uni-id-unbind-failed': '请先绑定后再解绑',
'uni-id-unbind-not-supported': '不支持解绑',
'uni-id-unbind-mobile-not-exists': '这是当前唯一登录方式,请绑定手机号后再尝试解绑',
'uni-id-unbind-password-not-exists': '请先设置密码在尝试解绑',
'uni-id-unsupported-request': '不支持的请求方式',
'uni-id-illegal-request': '非法请求',
'uni-id-frv-fail': '实名认证失败',
'uni-id-frv-processing': '等待人脸识别',
'uni-id-realname-verified': '该账号已实名认证',
'uni-id-idcard-exists': '该证件号码已绑定账号',
'uni-id-invalid-idcard': '身份证号码不合法',
'uni-id-invalid-realname': '姓名只能是汉字',
'uni-id-unknown-error': '未知错误',
'uni-id-realname-verify-upper-limit': '当日实名认证次数已达上限',
'uni-id-config-field-required': '缺少配置项: {field}',
'uni-id-config-field-invalid': '配置项: {field}无效'
}
module.exports = {
...word,
...sentence
}

View File

@ -0,0 +1,3 @@
# 说明
此目录内为uni-id-co基础能力不建议直接修改。如果你发现有些逻辑加入会更好或者此部分代码有Bug可以向我们提交PR仓库地址[]()。如果有特殊的需求也可以在[论坛](https://ask.dcloud.net.cn/)提出,我们可以讨论下如何实现。

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,16 @@
const AlipayBase = require('../alipayBase')
const protocols = require('./protocols')
module.exports = class Auth extends AlipayBase {
constructor (options) {
super(options)
this._protocols = protocols
}
async code2Session (code) {
const result = await this._exec('alipay.system.oauth.token', {
grantType: 'authorization_code',
code
})
return result
}
}

View File

@ -0,0 +1,10 @@
module.exports = {
code2Session: {
// args (fromArgs) {
// return fromArgs
// },
returnValue: {
openid: 'userId'
}
}
}

View File

@ -0,0 +1,231 @@
const {
camel2snakeJson,
snake2camelJson,
getOffsetDate,
getFullTimeStr
} = require('../../../common/utils')
const crypto = require('crypto')
const ALIPAY_ALGORITHM_MAPPING = {
RSA: 'RSA-SHA1',
RSA2: 'RSA-SHA256'
}
module.exports = class AlipayBase {
constructor (options = {}) {
if (!options.appId) throw new Error('appId required')
if (!options.privateKey) throw new Error('privateKey required')
const defaultOptions = {
gateway: 'https://openapi.alipay.com/gateway.do',
timeout: 5000,
charset: 'utf-8',
version: '1.0',
signType: 'RSA2',
timeOffset: -new Date().getTimezoneOffset() / 60,
keyType: 'PKCS8'
}
if (options.sandbox) {
options.gateway = 'https://openapi.alipaydev.com/gateway.do'
}
this.options = Object.assign({}, defaultOptions, options)
const privateKeyType =
this.options.keyType === 'PKCS8' ? 'PRIVATE KEY' : 'RSA PRIVATE KEY'
this.options.privateKey = this._formatKey(
this.options.privateKey,
privateKeyType
)
if (this.options.alipayPublicKey) {
this.options.alipayPublicKey = this._formatKey(
this.options.alipayPublicKey,
'PUBLIC KEY'
)
}
}
_formatKey (key, type) {
return `-----BEGIN ${type}-----\n${key}\n-----END ${type}-----`
}
_formatUrl (url, params) {
let requestUrl = url
// 需要放在 url 中的参数列表
const urlArgs = [
'app_id',
'method',
'format',
'charset',
'sign_type',
'sign',
'timestamp',
'version',
'notify_url',
'return_url',
'auth_token',
'app_auth_token'
]
for (const key in params) {
if (urlArgs.indexOf(key) > -1) {
const val = encodeURIComponent(params[key])
requestUrl = `${requestUrl}${requestUrl.includes('?') ? '&' : '?'
}${key}=${val}`
// 删除 postData 中对应的数据
delete params[key]
}
}
return { execParams: params, url: requestUrl }
}
_getSign (method, params) {
const bizContent = params.bizContent || null
delete params.bizContent
const signParams = Object.assign({
method,
appId: this.options.appId,
charset: this.options.charset,
version: this.options.version,
signType: this.options.signType,
timestamp: getFullTimeStr(getOffsetDate(this.options.timeOffset))
}, params)
if (bizContent) {
signParams.bizContent = JSON.stringify(camel2snakeJson(bizContent))
}
// params key 驼峰转下划线
const decamelizeParams = camel2snakeJson(signParams)
// 排序
const signStr = Object.keys(decamelizeParams)
.sort()
.map((key) => {
let data = decamelizeParams[key]
if (Array.prototype.toString.call(data) !== '[object String]') {
data = JSON.stringify(data)
}
return `${key}=${data}`
})
.join('&')
// 计算签名
const sign = crypto
.createSign(ALIPAY_ALGORITHM_MAPPING[this.options.signType])
.update(signStr, 'utf8')
.sign(this.options.privateKey, 'base64')
return Object.assign(decamelizeParams, { sign })
}
async _exec (method, params = {}, option = {}) {
// 计算签名
const signData = this._getSign(method, params)
const { url, execParams } = this._formatUrl(this.options.gateway, signData)
const { status, data } = await uniCloud.httpclient.request(url, {
method: 'POST',
data: execParams,
// 按 text 返回(为了验签)
dataType: 'text',
timeout: this.options.timeout
})
if (status !== 200) throw new Error('request fail')
/**
* 示例响应格式
* {"alipay_trade_precreate_response":
* {"code": "10000","msg": "Success","out_trade_no": "111111","qr_code": "https:\/\/"},
* "sign": "abcde="
* }
* 或者
* {"error_response":
* {"code":"40002","msg":"Invalid Arguments","sub_code":"isv.code-invalid","sub_msg":"授权码code无效"},
* }
*/
const result = JSON.parse(data)
const responseKey = `${method.replace(/\./g, '_')}_response`
const response = result[responseKey]
const errorResponse = result.error_response
if (response) {
// 按字符串验签
const validateSuccess = option.validateSign ? this._checkResponseSign(data, responseKey) : true
if (validateSuccess) {
if (!response.code || response.code === '10000') {
const errCode = 0
const errMsg = response.msg || ''
return {
errCode,
errMsg,
...snake2camelJson(response)
}
}
const msg = response.sub_code ? `${response.sub_code} ${response.sub_msg}` : `${response.msg || 'unkonwn error'}`
throw new Error(msg)
} else {
throw new Error('check sign error')
}
} else if (errorResponse) {
throw new Error(errorResponse.sub_msg || errorResponse.msg || 'request fail')
}
throw new Error('request fail')
}
_checkResponseSign (signStr, responseKey) {
if (!this.options.alipayPublicKey || this.options.alipayPublicKey === '') {
console.warn('options.alipayPublicKey is empty')
// 支付宝公钥不存在时不做验签
return true
}
// 带验签的参数不存在时返回失败
if (!signStr) { return false }
// 根据服务端返回的结果截取需要验签的目标字符串
const validateStr = this._getSignStr(signStr, responseKey)
// 服务端返回的签名
const serverSign = JSON.parse(signStr).sign
// 参数存在,并且是正常的结果(不包含 sub_code时才验签
const verifier = crypto.createVerify(ALIPAY_ALGORITHM_MAPPING[this.options.signType])
verifier.update(validateStr, 'utf8')
return verifier.verify(this.options.alipayPublicKey, serverSign, 'base64')
}
_getSignStr (originStr, responseKey) {
// 待签名的字符串
let validateStr = originStr.trim()
// 找到 xxx_response 开始的位置
const startIndex = originStr.indexOf(`${responseKey}"`)
// 找到最后一个 “"sign"” 字符串的位置(避免)
const lastIndex = originStr.lastIndexOf('"sign"')
/**
* 删除 xxx_response 及之前的字符串
* 假设原始字符串为
* {"xxx_response":{"code":"10000"},"sign":"jumSvxTKwn24G5sAIN"}
* 删除后变为
* :{"code":"10000"},"sign":"jumSvxTKwn24G5sAIN"}
*/
validateStr = validateStr.substr(startIndex + responseKey.length + 1)
/**
* 删除最后一个 "sign" 及之后的字符串
* 删除后变为
* :{"code":"10000"},
* {} 之间就是待验签的字符串
*/
validateStr = validateStr.substr(0, lastIndex)
// 删除第一个 { 之前的任何字符
validateStr = validateStr.replace(/^[^{]*{/g, '{')
// 删除最后一个 } 之后的任何字符
validateStr = validateStr.replace(/\}([^}]*)$/g, '}')
return validateStr
}
}

View File

@ -0,0 +1,79 @@
const rsaPublicKeyPem = require('../rsa-public-key-pem')
const {
jwtVerify
} = require('../../../npm/index')
let authKeysCache = null
module.exports = class Auth {
constructor (options) {
this.options = Object.assign({
baseUrl: 'https://appleid.apple.com',
timeout: 10000
}, options)
}
async _fetch (url, options) {
const { baseUrl } = this.options
return uniCloud.httpclient.request(baseUrl + url, options)
}
async verifyIdentityToken (identityToken) {
// 解密出kid拿取key
const jwtHeader = identityToken.split('.')[0]
const { kid } = JSON.parse(Buffer.from(jwtHeader, 'base64').toString())
let authKeys
if (authKeysCache) {
authKeys = authKeysCache
} else {
authKeys = await this.getAuthKeys()
authKeysCache = authKeys
}
const usedKey = authKeys.find(item => item.kid === kid)
/**
* identityToken 格式
*
* {
* iss: 'https://appleid.apple.com',
* aud: 'io.dcloud.hellouniapp',
* exp: 1610626724,
* iat: 1610540324,
* sub: '000628.30119d332d9b45a3be4a297f9391fd5c.0403',
* c_hash: 'oFfgewoG36cJX00KUbj45A',
* email: 'x2awmap99s@privaterelay.appleid.com',
* email_verified: 'true',
* is_private_email: 'true',
* auth_time: 1610540324,
* nonce_supported: true
* }
*/
const payload = jwtVerify(
identityToken,
rsaPublicKeyPem(usedKey.n, usedKey.e),
{
algorithms: usedKey.alg
}
)
if (payload.iss !== 'https://appleid.apple.com' || payload.aud !== this.options.bundleId) {
throw new Error('Invalid identity token')
}
return {
openid: payload.sub,
email: payload.email,
emailVerified: payload.email_verified === 'true',
isPrivateEmail: payload.is_private_email === 'true'
}
}
async getAuthKeys () {
const { status, data } = await this._fetch('/auth/keys', {
method: 'GET',
dataType: 'json',
timeout: this.options.timeout
})
if (status !== 200) throw new Error('request https://appleid.apple.com/auth/keys fail')
return data.keys
}
}

View File

@ -0,0 +1,64 @@
// http://stackoverflow.com/questions/18835132/xml-to-pem-in-node-js
/* eslint-disable camelcase */
function rsaPublicKeyPem (modulus_b64, exponent_b64) {
const modulus = Buffer.from(modulus_b64, 'base64')
const exponent = Buffer.from(exponent_b64, 'base64')
let modulus_hex = modulus.toString('hex')
let exponent_hex = exponent.toString('hex')
modulus_hex = prepadSigned(modulus_hex)
exponent_hex = prepadSigned(exponent_hex)
const modlen = modulus_hex.length / 2
const explen = exponent_hex.length / 2
const encoded_modlen = encodeLengthHex(modlen)
const encoded_explen = encodeLengthHex(explen)
const encoded_pubkey = '30' +
encodeLengthHex(
modlen +
explen +
encoded_modlen.length / 2 +
encoded_explen.length / 2 + 2
) +
'02' + encoded_modlen + modulus_hex +
'02' + encoded_explen + exponent_hex
const der_b64 = Buffer.from(encoded_pubkey, 'hex').toString('base64')
const pem = '-----BEGIN RSA PUBLIC KEY-----\n' +
der_b64.match(/.{1,64}/g).join('\n') +
'\n-----END RSA PUBLIC KEY-----\n'
return pem
}
function prepadSigned (hexStr) {
const msb = hexStr[0]
if (msb < '0' || msb > '7') {
return '00' + hexStr
} else {
return hexStr
}
}
function toHex (number) {
const nstr = number.toString(16)
if (nstr.length % 2) return '0' + nstr
return nstr
}
// encode ASN.1 DER length field
// if <=127, short form
// if >=128, long form
function encodeLengthHex (n) {
if (n <= 127) return toHex(n)
else {
const n_hex = toHex(n)
const length_of_length_byte = 128 + n_hex.length / 2 // 0x80+numbytes
return toHex(length_of_length_byte) + n_hex
}
}
module.exports = rsaPublicKeyPem

View File

@ -0,0 +1,36 @@
const WxAccount = require('./weixin/account/index')
const QQAccount = require('./qq/account/index')
const AliAccount = require('./alipay/account/index')
const AppleAccount = require('./apple/account/index')
const createApi = require('./share/create-api')
module.exports = {
initWeixin: function () {
const oauthConfig = this.configUtils.getOauthConfig({ provider: 'weixin' })
return createApi(WxAccount, {
appId: oauthConfig.appid,
secret: oauthConfig.appsecret
})
},
initQQ: function () {
const oauthConfig = this.configUtils.getOauthConfig({ provider: 'qq' })
return createApi(QQAccount, {
appId: oauthConfig.appid,
secret: oauthConfig.appsecret
})
},
initAlipay: function () {
const oauthConfig = this.configUtils.getOauthConfig({ provider: 'alipay' })
return createApi(AliAccount, {
appId: oauthConfig.appid,
privateKey: oauthConfig.privateKey
})
},
initApple: function () {
const oauthConfig = this.configUtils.getOauthConfig({ provider: 'apple' })
return createApi(AppleAccount, {
bundleId: oauthConfig.bundleId
})
}
}

View File

@ -0,0 +1,97 @@
const {
UniCloudError
} = require('../../../../common/error')
const {
resolveUrl
} = require('../../../../common/utils')
const {
callQQOpenApi
} = require('../normalize')
module.exports = class Auth {
constructor (options) {
this.options = Object.assign({
baseUrl: 'https://graph.qq.com',
timeout: 5000
}, options)
}
async _requestQQOpenapi ({ name, url, data, options }) {
const defaultOptions = {
method: 'GET',
dataType: 'json',
dataAsQueryString: true,
timeout: this.options.timeout
}
const result = await callQQOpenApi({
name: `auth.${name}`,
url: resolveUrl(this.options.baseUrl, url),
data,
options,
defaultOptions
})
return result
}
async getUserInfo ({
accessToken,
openid
} = {}) {
const url = '/user/get_user_info'
const result = await this._requestQQOpenapi({
name: 'getUserInfo',
url,
data: {
oauthConsumerKey: this.options.appId,
accessToken,
openid
}
})
return {
nickname: result.nickname,
avatar: result.figureurl_qq_1
}
}
async getOpenidByToken ({
accessToken
} = {}) {
const url = '/oauth2.0/me'
const result = await this._requestQQOpenapi({
name: 'getOpenidByToken',
url,
data: {
accessToken,
unionid: 1,
fmt: 'json'
}
})
if (result.clientId !== this.options.appId) {
throw new UniCloudError({
code: 'APPID_NOT_MATCH',
message: 'appid not match'
})
}
return {
openid: result.openid,
unionid: result.unionid
}
}
async code2Session ({
code
} = {}) {
const url = 'https://api.q.qq.com/sns/jscode2session'
const result = await this._requestQQOpenapi({
name: 'getOpenidByToken',
url,
data: {
grant_type: 'authorization_code',
appid: this.options.appId,
secret: this.options.secret,
js_code: code
}
})
return result
}
}

View File

@ -0,0 +1,85 @@
const {
UniCloudError
} = require('../../../common/error')
const {
camel2snakeJson,
snake2camelJson
} = require('../../../common/utils')
function generateApiResult (apiName, data) {
if (data.ret || data.error) {
// 这三种都是qq的错误码规范
const code = data.ret || data.error || data.errcode || -2
const message = data.msg || data.error_description || data.errmsg || `${apiName} fail`
throw new UniCloudError({
code,
message
})
} else {
delete data.ret
delete data.msg
delete data.error
delete data.error_description
delete data.errcode
delete data.errmsg
return {
...data,
errMsg: `${apiName} ok`,
errCode: 0
}
}
}
function nomalizeError (apiName, error) {
throw new UniCloudError({
code: error.code || -2,
message: error.message || `${apiName} fail`
})
}
async function callQQOpenApi ({
name,
url,
data,
options,
defaultOptions
}) {
options = Object.assign({}, defaultOptions, options, { data: camel2snakeJson(Object.assign({}, data)) })
let result
try {
result = await uniCloud.httpclient.request(url, options)
} catch (e) {
return nomalizeError(name, e)
}
let resData = result.data
const contentType = result.headers['content-type']
if (
Buffer.isBuffer(resData) &&
(contentType.indexOf('text/plain') === 0 ||
contentType.indexOf('application/json') === 0)
) {
try {
resData = JSON.parse(resData.toString())
} catch (e) {
resData = resData.toString()
}
} else if (Buffer.isBuffer(resData)) {
resData = {
buffer: resData,
contentType
}
}
return snake2camelJson(
generateApiResult(
name,
resData || {
errCode: -2,
errMsg: 'Request failed'
}
)
)
}
module.exports = {
callQQOpenApi
}

View File

@ -0,0 +1,73 @@
const {
isFn,
isPlainObject
} = require('../../../common/utils')
// 注意:不进行递归处理
function parseParams (params = {}, rule) {
if (!rule || !params) {
return params
}
const internalKeys = ['_pre', '_purify', '_post']
// 转换之前的处理
if (rule._pre) {
params = rule._pre(params)
}
// 净化参数
let purify = { shouldDelete: new Set([]) }
if (rule._purify) {
const _purify = rule._purify
for (const purifyKey in _purify) {
_purify[purifyKey] = new Set(_purify[purifyKey])
}
purify = Object.assign(purify, _purify)
}
if (isPlainObject(rule)) {
for (const key in rule) {
const parser = rule[key]
if (isFn(parser) && internalKeys.indexOf(key) === -1) {
params[key] = parser(params)
} else if (typeof parser === 'string' && internalKeys.indexOf(key) === -1) {
// 直接转换属性名称的删除旧属性名
params[key] = params[parser]
purify.shouldDelete.add(parser)
}
}
} else if (isFn(rule)) {
params = rule(params)
}
if (purify.shouldDelete) {
for (const item of purify.shouldDelete) {
delete params[item]
}
}
// 转换之后的处理
if (rule._post) {
params = rule._post(params)
}
return params
}
function createApi (ApiClass, options) {
const apiInstance = new ApiClass(options)
return new Proxy(apiInstance, {
get: function (obj, prop) {
if (typeof obj[prop] === 'function' && prop.indexOf('_') !== 0 && obj._protocols && obj._protocols[prop]) {
const protocol = obj._protocols[prop]
return async function (params) {
params = parseParams(params, protocol.args)
let result = await obj[prop](params)
result = parseParams(result, protocol.returnValue)
return result
}
} else {
return obj[prop]
}
}
})
}
module.exports = createApi

View File

@ -0,0 +1,111 @@
const {
callWxOpenApi,
buildUrl
} = require('../normalize')
module.exports = class Auth {
constructor (options) {
this.options = Object.assign({
baseUrl: 'https://api.weixin.qq.com',
timeout: 5000
}, options)
}
async _requestWxOpenapi ({ name, url, data, options }) {
const defaultOptions = {
method: 'GET',
dataType: 'json',
dataAsQueryString: true,
timeout: this.options.timeout
}
const result = await callWxOpenApi({
name: `auth.${name}`,
url: `${this.options.baseUrl}${buildUrl(url, data)}`,
data,
options,
defaultOptions
})
return result
}
async code2Session (code) {
const url = '/sns/jscode2session'
const result = await this._requestWxOpenapi({
name: 'code2Session',
url,
data: {
grant_type: 'authorization_code',
appid: this.options.appId,
secret: this.options.secret,
js_code: code
}
})
return result
}
async getOauthAccessToken (code) {
const url = '/sns/oauth2/access_token'
const result = await this._requestWxOpenapi({
name: 'getOauthAccessToken',
url,
data: {
grant_type: 'authorization_code',
appid: this.options.appId,
secret: this.options.secret,
code
}
})
if (result.expiresIn) {
result.expired = Date.now() + result.expiresIn * 1000
// delete result.expiresIn
}
return result
}
async getUserInfo ({
accessToken,
openid
} = {}) {
const url = '/sns/userinfo'
const {
nickname,
headimgurl: avatar
} = await this._requestWxOpenapi({
name: 'getUserInfo',
url,
data: {
accessToken,
openid,
appid: this.options.appId,
secret: this.options.secret,
scope: 'snsapi_userinfo'
}
})
return {
nickname,
avatar
}
}
async getPhoneNumber (accessToken, code) {
const url = `/wxa/business/getuserphonenumber?access_token=${accessToken}`
const { phoneInfo } = await this._requestWxOpenapi({
name: 'getPhoneNumber',
url,
data: {
code
},
options: {
method: 'POST',
dataAsQueryString: false,
headers: {
'content-type': 'application/json'
}
}
})
return {
purePhoneNumber: phoneInfo.purePhoneNumber
}
}
}

View File

@ -0,0 +1,95 @@
const {
UniCloudError
} = require('../../../common/error')
const {
camel2snakeJson, snake2camelJson
} = require('../../../common/utils')
function generateApiResult (apiName, data) {
if (data.errcode) {
throw new UniCloudError({
code: data.errcode || -2,
message: data.errmsg || `${apiName} fail`
})
} else {
delete data.errcode
delete data.errmsg
return {
...data,
errMsg: `${apiName} ok`,
errCode: 0
}
}
}
function nomalizeError (apiName, error) {
throw new UniCloudError({
code: error.code || -2,
message: error.message || `${apiName} fail`
})
}
// 微信openapi接口接收蛇形snake case参数返回蛇形参数这里进行转化如果是formdata里面的参数需要在对应api实现时就转为蛇形
async function callWxOpenApi ({
name,
url,
data,
options,
defaultOptions
}) {
let result = {}
// 获取二维码的接口wxacode.get和wxacode.getUnlimited不可以传入access_token可能有其他接口也不可以否则会返回data format error
const dataCopy = camel2snakeJson(Object.assign({}, data))
if (dataCopy && dataCopy.access_token) {
delete dataCopy.access_token
}
try {
options = Object.assign({}, defaultOptions, options, { data: dataCopy })
result = await uniCloud.httpclient.request(url, options)
} catch (e) {
return nomalizeError(name, e)
}
// 有几个接口成功返回buffer失败返回json对这些接口统一成返回buffer然后分别解析
let resData = result.data
const contentType = result.headers['content-type']
if (
Buffer.isBuffer(resData) &&
(contentType.indexOf('text/plain') === 0 ||
contentType.indexOf('application/json') === 0)
) {
try {
resData = JSON.parse(resData.toString())
} catch (e) {
resData = resData.toString()
}
} else if (Buffer.isBuffer(resData)) {
resData = {
buffer: resData,
contentType
}
}
return snake2camelJson(
generateApiResult(
name,
resData || {
errCode: -2,
errMsg: 'Request failed'
}
)
)
}
function buildUrl (url, data) {
let query = ''
if (data && data.accessToken) {
const divider = url.indexOf('?') > -1 ? '&' : '?'
query = `${divider}access_token=${data.accessToken}`
}
return `${url}${query}`
}
module.exports = {
callWxOpenApi,
buildUrl
}

View File

@ -0,0 +1,87 @@
const crypto = require('crypto')
const {
isPlainObject
} = require('../../../common/utils')
// 退款通知解密key=md5(key)
function decryptData (encryptedData, key, iv = '') {
// 解密
const decipher = crypto.createDecipheriv('aes-128-cbc', key, iv)
// 设置自动 padding 为 true删除填充补位
decipher.setAutoPadding(true)
let decoded = decipher.update(encryptedData, 'base64', 'utf8')
decoded += decipher.final('utf8')
return decoded
}
function md5 (str, encoding = 'utf8') {
return crypto
.createHash('md5')
.update(str, encoding)
.digest('hex')
}
function sha256 (str, key, encoding = 'utf8') {
return crypto
.createHmac('sha256', key)
.update(str, encoding)
.digest('hex')
}
function getSignStr (obj) {
return Object.keys(obj)
.filter(key => key !== 'sign' && obj[key] !== undefined && obj[key] !== '')
.sort()
.map(key => key + '=' + obj[key])
.join('&')
}
function getNonceStr (length = 16) {
let str = ''
while (str.length < length) {
str += Math.random().toString(32).substring(2)
}
return str.substring(0, length)
}
// 简易版Object转XML只可在微信支付时使用不支持嵌套
function buildXML (obj, rootName = 'xml') {
const content = Object.keys(obj).map(item => {
if (isPlainObject(obj[item])) {
return `<${item}><![CDATA[${JSON.stringify(obj[item])}]]></${item}>`
} else {
return `<${item}><![CDATA[${obj[item]}]]></${item}>`
}
})
return `<${rootName}>${content.join('')}</${rootName}>`
}
function isXML (str) {
const reg = /^(<\?xml.*\?>)?(\r?\n)*<xml>(.|\r?\n)*<\/xml>$/i
return reg.test(str.trim())
};
// 简易版XML转Object只可在微信支付时使用不支持嵌套
function parseXML (xml) {
const xmlReg = /<(?:xml|root).*?>([\s|\S]*)<\/(?:xml|root)>/
const str = xmlReg.exec(xml)[1]
const obj = {}
const nodeReg = /<(.*?)>(?:<!\[CDATA\[){0,1}(.*?)(?:\]\]>){0,1}<\/.*?>/g
let matches = null
// eslint-disable-next-line no-cond-assign
while ((matches = nodeReg.exec(str))) {
obj[matches[1]] = matches[2]
}
return obj
}
module.exports = {
decryptData,
md5,
sha256,
getSignStr,
getNonceStr,
buildXML,
parseXML,
isXML
}

View File

@ -0,0 +1,98 @@
const {
dbCmd,
userCollection
} = require('../../common/constants')
const {
USER_IDENTIFIER
} = require('../../common/constants')
const {
batchFindObjctValue,
getType,
isMatchUserApp
} = require('../../common/utils')
/**
* 查询满足条件的用户
* @param {Object} params
* @param {Object} params.userQuery 用户唯一标识组成的查询条件
* @param {Object} params.authorizedApp 用户允许登录的应用
* @returns userMatched 满足条件的用户列表
*/
async function findUser (params = {}) {
const {
userQuery,
authorizedApp = []
} = params
const condition = getUserQueryCondition(userQuery)
if (condition.length === 0) {
throw new Error('Invalid user query')
}
const authorizedAppType = getType(authorizedApp)
if (authorizedAppType !== 'string' && authorizedAppType !== 'array') {
throw new Error('Invalid authorized app')
}
let finalQuery
if (condition.length === 1) {
finalQuery = condition[0]
} else {
finalQuery = dbCmd.or(condition)
}
const userQueryRes = await userCollection.where(finalQuery).get()
return {
total: userQueryRes.data.length,
userMatched: userQueryRes.data.filter(item => {
return isMatchUserApp(item.dcloud_appid, authorizedApp)
})
}
}
function getUserIdentifier (userRecord = {}) {
const keys = Object.keys(USER_IDENTIFIER)
return batchFindObjctValue(userRecord, keys)
}
function getUserQueryCondition (userRecord = {}) {
const userIdentifier = getUserIdentifier(userRecord)
const condition = []
for (const key in userIdentifier) {
const value = userIdentifier[key]
if (!value) {
// 过滤所有value为假值的条件在查询用户时没有意义
continue
}
const queryItem = {
[key]: value
}
// 为兼容用户老数据用户名及邮箱需要同时查小写及原始大小写数据
if (key === 'mobile') {
queryItem.mobile_confirmed = 1
} else if (key === 'email') {
queryItem.email_confirmed = 1
const email = userIdentifier.email
if (email.toLowerCase() !== email) {
condition.push({
email: email.toLowerCase(),
email_confirmed: 1
})
}
} else if (key === 'username') {
const username = userIdentifier.username
if (username.toLowerCase() !== username) {
condition.push({
username: username.toLowerCase()
})
}
} else if (key === 'identities') {
queryItem.identities = dbCmd.elemMatch(value)
}
condition.push(queryItem)
}
return condition
}
module.exports = {
findUser,
getUserIdentifier
}

View File

@ -0,0 +1,76 @@
const {
ERROR
} = require('../../common/error')
async function getNeedCaptcha ({
uid,
username,
mobile,
email,
type = 'login',
limitDuration = 7200000, // 两小时
limitTimes = 3 // 记录次数
} = {}) {
const db = uniCloud.database()
const dbCmd = db.command
// 当用户最近“2小时内(limitDuration)”登录失败达到3次(limitTimes)时。要求用户提交验证码
const now = Date.now()
const uniIdLogCollection = db.collection('uni-id-log')
const userIdentifier = {
user_id: uid,
username,
mobile,
email
}
let totalKey = 0; let deleteKey = 0
for (const key in userIdentifier) {
totalKey++
if (!userIdentifier[key] || typeof userIdentifier[key] !== 'string') {
deleteKey++
delete userIdentifier[key]
}
}
if (deleteKey === totalKey) {
throw new Error('System error') // 正常情况下不会进入此条件,但是考虑到后续会有其他开发者修改此云对象,在此处做一个判断
}
const {
data: recentRecord
} = await uniIdLogCollection.where({
ip: this.getUniversalClientInfo().clientIP,
...userIdentifier,
type,
create_date: dbCmd.gt(now - limitDuration)
})
.orderBy('create_date', 'desc')
.limit(limitTimes)
.get()
return recentRecord.length === limitTimes && recentRecord.every(item => item.state === 0)
}
async function verifyCaptcha (params = {}) {
const {
captcha,
scene
} = params
if (!captcha) {
throw {
errCode: ERROR.CAPTCHA_REQUIRED
}
}
const payload = await this.uniCaptcha.verify({
deviceId: this.getUniversalClientInfo().deviceId,
captcha,
scene
})
if (payload.errCode) {
throw payload
}
}
module.exports = {
getNeedCaptcha,
verifyCaptcha
}

View File

@ -0,0 +1,137 @@
const {
getWeixinPlatform
} = require('./weixin')
const createConfig = require('uni-config-center')
const requiredConfig = {
'web.weixin-h5': ['appid', 'appsecret'],
'web.weixin-web': ['appid', 'appsecret'],
'app.weixin': ['appid', 'appsecret'],
'mp-weixin.weixin': ['appid', 'appsecret'],
'app.qq': ['appid', 'appsecret'],
'mp-alipay.alipay': ['appid', 'privateKey'],
'app.apple': ['bundleId']
}
const uniIdConfig = createConfig({
pluginId: 'uni-id'
})
class ConfigUtils {
constructor({
context
} = {}) {
this.context = context
this.clientInfo = context.getUniversalClientInfo()
const {
appId,
uniPlatform
} = this.clientInfo
this.appId = appId
switch (uniPlatform) {
case 'app':
case 'app-plus':
case 'app-android':
case 'app-ios':
this.platform = 'app'
break
case 'web':
case 'h5':
this.platform = 'web'
break
default:
this.platform = uniPlatform
break
}
}
getConfigArray() {
let configContent
try {
configContent = require('uni-config-center/uni-id/config.json')
} catch (error) {
throw new Error('Invalid config file\n' + error.message)
}
if (configContent[0]) {
return Object.values(configContent)
}
configContent.isDefaultConfig = true
return [configContent]
}
getAppConfig() {
const configArray = this.getConfigArray()
return configArray.find(item => item.dcloudAppid === this.appId) || configArray.find(item => item.isDefaultConfig)
}
getPlatformConfig() {
const appConfig = this.getAppConfig()
if (!appConfig) {
throw new Error(
`Config for current app (${this.appId}) was not found, please check your config file or client appId`)
}
const platform = this.platform
if (
(this.platform === 'app' && appConfig['app-plus']) ||
(this.platform === 'web' && appConfig.h5)
) {
throw new Error(
`Client platform is ${this.platform}, but ${this.platform === 'web' ? 'h5' : 'app-plus'} was found in config. Please refer to: https://uniapp.dcloud.net.cn/uniCloud/uni-id-summary?id=m-to-co`
)
}
const defaultConfig = {
tokenExpiresIn: 7200,
tokenExpiresThreshold: 1200,
passwordErrorLimit: 6,
passwordErrorRetryTime: 3600
}
return Object.assign(defaultConfig, appConfig, appConfig[platform])
}
getOauthProvider({
provider
} = {}) {
const clientPlatform = this.platform
let oatuhProivder = provider
if (provider === 'weixin' && clientPlatform === 'web') {
const weixinPlatform = getWeixinPlatform.call(this.context)
if (weixinPlatform === 'h5' || weixinPlatform === 'web') {
oatuhProivder = 'weixin-' + weixinPlatform // weixin-h5 公众号weixin-web pc端
}
}
return oatuhProivder
}
getOauthConfig({
provider
} = {}) {
const config = this.getPlatformConfig()
const clientPlatform = this.platform
const oatuhProivder = this.getOauthProvider({
provider
})
const requireConfigKey = requiredConfig[`${clientPlatform}.${oatuhProivder}`] || []
if (!config.oauth || !config.oauth[oatuhProivder]) {
throw new Error(`Config param required: ${clientPlatform}.oauth.${oatuhProivder}`)
}
const oauthConfig = config.oauth[oatuhProivder]
requireConfigKey.forEach((item) => {
if (!oauthConfig[item]) {
throw new Error(`Config param required: ${clientPlatform}.oauth.${oatuhProivder}.${item}`)
}
})
return oauthConfig
}
getHooks() {
if (uniIdConfig.hasFile('hooks/index.js')) {
return require(
uniIdConfig.resolve('hooks/index.js')
)
}
return {}
}
}
module.exports = ConfigUtils

View File

@ -0,0 +1,192 @@
const {
dbCmd,
userCollection
} = require('../../common/constants')
const {
ERROR
} = require('../../common/error')
/**
* 获取随机邀请码,邀请码由大写字母加数字组成,由于存在手动输入邀请码的场景,从可选字符中去除 0、1、I、O
* @param {number} len 邀请码长度默认6位
* @returns {string} 随机邀请码
*/
function getRandomInviteCode (len = 6) {
const charArr = ['2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'J', 'K', 'L', 'M', 'N', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z']
let code = ''
for (let i = 0; i < len; i++) {
code += charArr[Math.floor(Math.random() * charArr.length)]
}
return code
}
/**
* 获取可用的邀请码至多尝试十次以获取可用邀请码。从10亿可选值中随机碰撞概率较低
* 也有其他方案可以尝试比如在数据库内设置一个从0开始计数的数字每次调用此方法时使用updateAndReturn使数字加1并返回加1后的值根据这个值生成对应的邀请码比如22222A + 1 == 22222B此方式性能理论更好但是不适用于旧项目
* @param {object} param
* @param {string} param.inviteCode 初始随机邀请码
*/
async function getValidInviteCode () {
let retry = 10
let code
let codeValid = false
while (retry > 0 && !codeValid) {
retry--
code = getRandomInviteCode()
const getUserRes = await userCollection.where({
my_invite_code: code
}).limit(1).get()
if (getUserRes.data.length === 0) {
codeValid = true
break
}
}
if (!codeValid) {
throw {
errCode: ERROR.SET_INVITE_CODE_FAILED
}
}
return code
}
/**
* 根据邀请码查询邀请人
* @param {object} param
* @param {string} param.inviteCode 邀请码
* @param {string} param.queryUid 受邀人id非空时校验不可被下家或自己邀请
* @returns
*/
async function findUserByInviteCode ({
inviteCode,
queryUid
} = {}) {
if (typeof inviteCode !== 'string') {
throw {
errCode: ERROR.SYSTEM_ERROR
}
}
// 根据邀请码查询邀请人
let getInviterRes
if (queryUid) {
getInviterRes = await userCollection.where({
_id: dbCmd.neq(queryUid),
inviter_uid: dbCmd.not(dbCmd.all([queryUid])),
my_invite_code: inviteCode
}).get()
} else {
getInviterRes = await userCollection.where({
my_invite_code: inviteCode
}).get()
}
if (getInviterRes.data.length > 1) {
// 正常情况下不可能进入此条件,以防用户自行修改数据库出错,在此做出判断
throw {
errCode: ERROR.SYSTEM_ERROR
}
}
const inviterRecord = getInviterRes.data[0]
if (!inviterRecord) {
throw {
errCode: ERROR.INVALID_INVITE_CODE
}
}
return inviterRecord
}
/**
* 根据邀请码生成邀请信息
* @param {object} param
* @param {string} param.inviteCode 邀请码
* @param {string} param.queryUid 受邀人id非空时校验不可被下家或自己邀请
* @returns
*/
async function generateInviteInfo ({
inviteCode,
queryUid
} = {}) {
const inviterRecord = await findUserByInviteCode({
inviteCode,
queryUid
})
// 倒叙拼接当前用户邀请链
const inviterUid = inviterRecord.inviter_uid || []
inviterUid.unshift(inviterRecord._id)
return {
inviterUid,
inviteTime: Date.now()
}
}
/**
* 检查当前用户是否可以接受邀请,如果可以返回用户记录
* @param {string} uid
*/
async function checkInviteInfo (uid) {
// 检查当前用户是否已有邀请人
const getUserRes = await userCollection.doc(uid).field({
my_invite_code: true,
inviter_uid: true
}).get()
const userRecord = getUserRes.data[0]
if (!userRecord) {
throw {
errCode: ERROR.ACCOUNT_NOT_EXISTS
}
}
if (userRecord.inviter_uid && userRecord.inviter_uid.length > 0) {
throw {
errCode: ERROR.CHANGE_INVITER_FORBIDDEN
}
}
return userRecord
}
/**
* 指定用户接受邀请码邀请
* @param {object} param
* @param {string} param.uid 用户uid
* @param {string} param.inviteCode 邀请人的邀请码
* @returns
*/
async function acceptInvite ({
uid,
inviteCode
} = {}) {
await checkInviteInfo(uid)
const {
inviterUid,
inviteTime
} = await generateInviteInfo({
inviteCode,
queryUid: uid
})
if (inviterUid === uid) {
throw {
errCode: ERROR.INVALID_INVITE_CODE
}
}
// 更新当前用户的邀请人信息
await userCollection.doc(uid).update({
inviter_uid: inviterUid,
invite_time: inviteTime
})
// 更新当前用户邀请的用户的邀请人信息,这步可能较为耗时
await userCollection.where({
inviter_uid: uid
}).update({
inviter_uid: dbCmd.push(inviterUid)
})
return {
errCode: 0,
errMsg: ''
}
}
module.exports = {
acceptInvite,
generateInviteInfo,
getValidInviteCode
}

View File

@ -0,0 +1,246 @@
const {
findUser
} = require('./account')
const {
userCollection,
LOG_TYPE
} = require('../../common/constants')
const {
ERROR
} = require('../../common/error')
const {
logout
} = require('./logout')
const PasswordUtils = require('./password')
async function realPreLogin (params = {}) {
const {
user
} = params
const appId = this.getUniversalClientInfo().appId
const {
total,
userMatched
} = await findUser({
userQuery: user,
authorizedApp: appId
})
if (userMatched.length === 0) {
if (total > 0) {
throw {
errCode: ERROR.ACCOUNT_NOT_EXISTS_IN_CURRENT_APP
}
}
throw {
errCode: ERROR.ACCOUNT_NOT_EXISTS
}
} else if (userMatched.length > 1) {
throw {
errCode: ERROR.ACCOUNT_CONFLICT
}
}
const userRecord = userMatched[0]
checkLoginUserRecord(userRecord)
return userRecord
}
async function preLogin (params = {}) {
const {
user
} = params
try {
const user = await realPreLogin.call(this, params)
return user
} catch (error) {
await this.middleware.uniIdLog({
success: false,
data: user,
type: LOG_TYPE.LOGIN
})
throw error
}
}
async function preLoginWithPassword (params = {}) {
const {
user,
password
} = params
try {
const userRecord = await realPreLogin.call(this, params)
const {
passwordErrorLimit,
passwordErrorRetryTime
} = this.config
const {
clientIP
} = this.getUniversalClientInfo()
// 根据ip地址密码错误次数过多锁定登录
let loginIPLimit = userRecord.login_ip_limit || []
// 清理无用记录
loginIPLimit = loginIPLimit.filter(item => item.last_error_time > Date.now() - passwordErrorRetryTime * 1000)
let currentIPLimit = loginIPLimit.find(item => item.ip === clientIP)
if (currentIPLimit && currentIPLimit.error_times >= passwordErrorLimit) {
throw {
errCode: ERROR.PASSWORD_ERROR_EXCEED_LIMIT
}
}
const passwordUtils = new PasswordUtils({
userRecord,
clientInfo: this.getUniversalClientInfo(),
passwordSecret: this.config.passwordSecret
})
const {
success: checkPasswordSuccess,
refreshPasswordInfo
} = passwordUtils.checkUserPassword({
password
})
if (!checkPasswordSuccess) {
// 更新用户ip对应的密码错误记录
if (!currentIPLimit) {
currentIPLimit = {
ip: clientIP,
error_times: 1,
last_error_time: Date.now()
}
loginIPLimit.push(currentIPLimit)
} else {
currentIPLimit.error_times++
currentIPLimit.last_error_time = Date.now()
}
await userCollection.doc(userRecord._id).update({
login_ip_limit: loginIPLimit
})
throw {
errCode: ERROR.PASSWORD_ERROR
}
}
const extraData = {}
if (refreshPasswordInfo) {
extraData.password = refreshPasswordInfo.passwordHash
extraData.password_secret_version = refreshPasswordInfo.version
}
const currentIPLimitIndex = loginIPLimit.indexOf(currentIPLimit)
if (currentIPLimitIndex > -1) {
loginIPLimit.splice(currentIPLimitIndex, 1)
}
extraData.login_ip_limit = loginIPLimit
return {
user: userRecord,
extraData
}
} catch (error) {
await this.middleware.uniIdLog({
success: false,
data: user,
type: LOG_TYPE.LOGIN
})
throw error
}
}
function checkLoginUserRecord (user) {
switch (user.status) {
case undefined:
case 0:
break
case 1:
throw {
errCode: ERROR.ACCOUNT_BANNED
}
case 2:
throw {
errCode: ERROR.ACCOUNT_AUDITING
}
case 3:
throw {
errCode: ERROR.ACCOUNT_AUDIT_FAILED
}
case 4:
throw {
errCode: ERROR.ACCOUNT_CLOSED
}
default:
break
}
}
async function thirdPartyLogin (params = {}) {
const {
user
} = params
return {
mobileConfirmed: !!user.mobile_confirmed,
emailConfirmed: !!user.email_confirmed
}
}
async function postLogin (params = {}) {
const {
user,
extraData,
isThirdParty = false
} = params
const {
clientIP
} = this.getUniversalClientInfo()
const uniIdToken = this.getUniversalUniIdToken()
const uid = user._id
const updateData = {
last_login_date: Date.now(),
last_login_ip: clientIP,
...extraData
}
const createTokenRes = await this.uniIdCommon.createToken({
uid
})
const {
errCode,
token,
tokenExpired
} = createTokenRes
if (errCode) {
throw createTokenRes
}
if (uniIdToken) {
try {
await logout.call(this)
} catch (error) {}
}
await userCollection.doc(uid).update(updateData)
await this.middleware.uniIdLog({
data: {
user_id: uid
},
type: LOG_TYPE.LOGIN
})
return {
errCode: 0,
newToken: {
token,
tokenExpired
},
uid,
...(
isThirdParty
? thirdPartyLogin({
user
})
: {}
),
passwordConfirmed: !!user.password
}
}
module.exports = {
preLogin,
postLogin,
checkLoginUserRecord,
preLoginWithPassword
}

View File

@ -0,0 +1,49 @@
const {
dbCmd,
LOG_TYPE,
deviceCollection,
userCollection
} = require('../../common/constants')
async function logout () {
const {
deviceId
} = this.getUniversalClientInfo()
const uniIdToken = this.getUniversalUniIdToken()
const payload = await this.uniIdCommon.checkToken(
uniIdToken,
{
autoRefresh: false
}
)
if (payload.errCode) {
throw payload
}
const uid = payload.uid
// 删除token
await userCollection.doc(uid).update({
token: dbCmd.pull(uniIdToken)
})
// 仅当device表的device_id和user_id均对应时才进行更新
await deviceCollection.where({
device_id: deviceId,
user_id: uid
}).update({
token_expired: 0
})
await this.middleware.uniIdLog({
data: {
user_id: uid
},
type: LOG_TYPE.LOGOUT
})
return {
errCode: 0
}
}
module.exports = {
logout
}

View File

@ -0,0 +1,261 @@
const {
getType
} = require('../../common/utils')
const crypto = require('crypto')
const createConfig = require('uni-config-center')
const shareConfig = createConfig({
pluginId: 'uni-id'
})
let customPassword = {}
if (shareConfig.hasFile('custom-password.js')) {
customPassword = shareConfig.requireFile('custom-password.js') || {}
}
const passwordAlgorithmMap = {
UNI_ID_HMAC_SHA1: 'hmac-sha1',
UNI_ID_HMAC_SHA256: 'hmac-sha256',
UNI_ID_CUSTOM: 'custom'
}
const passwordAlgorithmKeyMap = Object.keys(passwordAlgorithmMap).reduce((res, item) => {
res[passwordAlgorithmMap[item]] = item
return res
}, {})
const passwordExtMethod = {
[passwordAlgorithmMap.UNI_ID_HMAC_SHA1]: {
verify ({ password }) {
const { password_secret_version: passwordSecretVersion } = this.userRecord
const passwordSecret = this._getSecretByVersion({
version: passwordSecretVersion
})
const { passwordHash } = this.encrypt({
password,
passwordSecret
})
return passwordHash === this.userRecord.password
},
encrypt ({ password, passwordSecret }) {
const { value: secret, version } = passwordSecret
const hmac = crypto.createHmac('sha1', secret.toString('ascii'))
hmac.update(password)
return {
passwordHash: hmac.digest('hex'),
version
}
}
},
[passwordAlgorithmMap.UNI_ID_HMAC_SHA256]: {
verify ({ password }) {
const parse = this._parsePassword()
const passwordHash = crypto.createHmac(parse.algorithm, parse.salt).update(password).digest('hex')
return passwordHash === parse.hash
},
encrypt ({ password, passwordSecret }) {
const { version } = passwordSecret
// 默认使用 sha256 加密算法
const salt = crypto.randomBytes(10).toString('hex')
const sha256Hash = crypto.createHmac(passwordAlgorithmMap.UNI_ID_HMAC_SHA256.substring(5), salt).update(password).digest('hex')
const algorithm = passwordAlgorithmKeyMap[passwordAlgorithmMap.UNI_ID_HMAC_SHA256]
// B 为固定值,对应 PasswordMethodMaps 中的 sha256算法
// hash 格式 $[PasswordMethodFlagMapsKey]$[salt size]$[salt][Hash]
const passwordHash = `$${algorithm}$${salt.length}$${salt}${sha256Hash}`
return {
passwordHash,
version
}
}
},
[passwordAlgorithmMap.UNI_ID_CUSTOM]: {
verify ({ password, passwordSecret }) {
if (!customPassword.verifyPassword) throw new Error('verifyPassword method not found in custom password file')
// return true or false
return customPassword.verifyPassword({
password,
passwordSecret,
userRecord: this.userRecord,
clientInfo: this.clientInfo
})
},
encrypt ({ password, passwordSecret }) {
if (!customPassword.encryptPassword) throw new Error('encryptPassword method not found in custom password file')
// return object<{passwordHash: string, version: number}>
return customPassword.encryptPassword({
password,
passwordSecret,
clientInfo: this.clientInfo
})
}
}
}
class PasswordUtils {
constructor ({
userRecord = {},
clientInfo,
passwordSecret
} = {}) {
if (!clientInfo) throw new Error('Invalid clientInfo')
if (!passwordSecret) throw new Error('Invalid password secret')
this.clientInfo = clientInfo
this.userRecord = userRecord
this.passwordSecret = this.prePasswordSecret(passwordSecret)
}
/**
* passwordSecret 预处理
* @param passwordSecret
* @return {*[]}
*/
prePasswordSecret (passwordSecret) {
const newPasswordSecret = []
if (getType(passwordSecret) === 'string') {
newPasswordSecret.push({
value: passwordSecret,
type: passwordAlgorithmMap.UNI_ID_HMAC_SHA1
})
} else if (getType(passwordSecret) === 'array') {
for (const secret of passwordSecret.sort((a, b) => a.version - b.version)) {
newPasswordSecret.push({
...secret,
// 没有 type 设置默认 type hmac-sha1
type: secret.type || passwordAlgorithmMap.UNI_ID_HMAC_SHA1
})
}
} else {
throw new Error('Invalid password secret')
}
return newPasswordSecret
}
/**
* 获取最新加密密钥
* @return {*}
* @private
*/
_getLastestSecret () {
return this.passwordSecret[this.passwordSecret.length - 1]
}
_getOldestSecret () {
return this.passwordSecret[0]
}
_getSecretByVersion ({ version } = {}) {
if (!version && version !== 0) {
return this._getOldestSecret()
}
if (this.passwordSecret.length === 1) {
return this.passwordSecret[0]
}
return this.passwordSecret.find(item => item.version === version)
}
/**
* 获取密码的验证/加密方法
* @param passwordSecret
* @return {*[]}
* @private
*/
_getPasswordExt (passwordSecret) {
const ext = passwordExtMethod[passwordSecret.type]
if (!ext) {
throw new Error(`暂不支持 ${passwordSecret.type} 类型的加密算法`)
}
const passwordExt = Object.create(null)
for (const key in ext) {
passwordExt[key] = ext[key].bind(Object.assign(this, Object.keys(ext).reduce((res, item) => {
if (item !== key) {
res[item] = ext[item].bind(this)
}
return res
}, {})))
}
return passwordExt
}
_parsePassword () {
const [algorithmKey = '', cost = 0, hashStr = ''] = this.userRecord.password.split('$').filter(key => key)
const algorithm = passwordAlgorithmMap[algorithmKey] ? passwordAlgorithmMap[algorithmKey].substring(5) : null
const salt = hashStr.substring(0, Number(cost))
const hash = hashStr.substring(Number(cost))
return {
algorithm,
salt,
hash
}
}
/**
* 生成加密后的密码
* @param {String} password 密码
*/
generatePasswordHash ({ password }) {
if (!password) throw new Error('Invalid password')
const passwordSecret = this._getLastestSecret()
const ext = this._getPasswordExt(passwordSecret)
const { passwordHash, version } = ext.encrypt({
password,
passwordSecret
})
return {
passwordHash,
version
}
}
/**
* 密码校验
* @param {String} password
* @param {Boolean} autoRefresh
* @return {{refreshPasswordInfo: {version: *, passwordHash: *}, success: boolean}|{success: boolean}}
*/
checkUserPassword ({ password, autoRefresh = true }) {
if (!password) throw new Error('Invalid password')
const { password_secret_version: passwordSecretVersion } = this.userRecord
const passwordSecret = this._getSecretByVersion({
version: passwordSecretVersion
})
const ext = this._getPasswordExt(passwordSecret)
const success = ext.verify({ password, passwordSecret })
if (!success) {
return {
success: false
}
}
let refreshPasswordInfo
if (autoRefresh && passwordSecretVersion !== this._getLastestSecret().version) {
refreshPasswordInfo = this.generatePasswordHash({ password })
}
return {
success: true,
refreshPasswordInfo
}
}
}
module.exports = PasswordUtils

View File

@ -0,0 +1,154 @@
const {
userCollection
} = require('../../common/constants')
const {
ERROR
} = require('../../common/error')
function getQQPlatform () {
const platform = this.clientPlatform
switch (platform) {
case 'app':
case 'app-plus':
case 'app-android':
case 'app-ios':
return 'app'
case 'mp-qq':
return 'mp'
default:
throw new Error('Unsupported qq platform')
}
}
async function saveQQUserKey ({
openid,
sessionKey, // QQ小程序用户sessionKey
accessToken, // App端QQ用户accessToken
accessTokenExpired // App端QQ用户accessToken过期时间
} = {}) {
// 微信公众平台、开放平台refreshToken有效期均为30天微信没有在网络请求里面返回30天这个值务必注意未来可能出现调整需及时更新此处逻辑
// 此前QQ开放平台有调整过accessToken的过期时间[access_token有效期由90天缩短至30天](https://wiki.connect.qq.com/%E3%80%90qq%E4%BA%92%E8%81%94%E3%80%91access_token%E6%9C%89%E6%95%88%E6%9C%9F%E8%B0%83%E6%95%B4)
const appId = this.getUniversalClientInfo().appId
const qqPlatform = getQQPlatform.call(this)
const keyObj = {
dcloudAppid: appId,
openid,
platform: 'qq-' + qqPlatform
}
switch (qqPlatform) {
case 'mp':
await this.uniOpenBridge.setSessionKey(keyObj, {
session_key: sessionKey
}, 30 * 24 * 60 * 60)
break
case 'app':
case 'h5':
case 'web':
await this.uniOpenBridge.setUserAccessToken(keyObj, {
access_token: accessToken,
access_token_expired: accessTokenExpired
}, accessTokenExpired
? Math.floor((accessTokenExpired - Date.now()) / 1000)
: 30 * 24 * 60 * 60
)
break
default:
break
}
}
function generateQQCache ({
sessionKey, // QQ小程序用户sessionKey
accessToken, // App端QQ用户accessToken
accessTokenExpired // App端QQ用户accessToken过期时间
} = {}) {
const platform = getQQPlatform.call(this)
let cache
switch (platform) {
case 'app':
cache = {
access_token: accessToken,
access_token_expired: accessTokenExpired
}
break
case 'mp':
cache = {
session_key: sessionKey
}
break
default:
throw new Error('Unsupported qq platform')
}
return {
third_party: {
[`${platform}_qq`]: cache
}
}
}
function getQQOpenid ({
userRecord
} = {}) {
const qqPlatform = getQQPlatform.call(this)
const appId = this.getUniversalClientInfo().appId
const qqOpenidObj = userRecord.qq_openid
if (!qqOpenidObj) {
return
}
return qqOpenidObj[`${qqPlatform}_${appId}`] || qqOpenidObj[qqPlatform]
}
async function getQQCacheFallback ({
userRecord,
key
} = {}) {
const platform = getQQPlatform.call(this)
const thirdParty = userRecord && userRecord.third_party
if (!thirdParty) {
return
}
const qqCache = thirdParty[`${platform}_qq`]
return qqCache && qqCache[key]
}
async function getQQCache ({
uid,
userRecord,
key
} = {}) {
const qqPlatform = getQQPlatform.call(this)
const appId = this.getUniversalClientInfo().appId
if (!userRecord) {
const getUserRes = await userCollection.doc(uid).get()
userRecord = getUserRes.data[0]
}
if (!userRecord) {
throw {
errCode: ERROR.ACCOUNT_NOT_EXISTS
}
}
const openid = getQQOpenid.call(this, {
userRecord
})
const getCacheMethod = qqPlatform === 'mp' ? 'getSessionKey' : 'getUserAccessToken'
const userKey = await this.uniOpenBridge[getCacheMethod]({
dcloudAppid: appId,
platform: 'qq-' + qqPlatform,
openid
})
if (userKey) {
return userKey[key]
}
return getQQCacheFallback({
userRecord,
key
})
}
module.exports = {
getQQPlatform,
generateQQCache,
getQQCache,
saveQQUserKey
}

View File

@ -0,0 +1,231 @@
const {
userCollection,
LOG_TYPE
} = require('../../common/constants')
const {
ERROR
} = require('../../common/error')
const {
findUser
} = require('./account')
const {
getValidInviteCode,
generateInviteInfo
} = require('./fission')
const {
logout
} = require('./logout')
const PasswordUtils = require('./password')
const {
merge
} = require('../npm/index')
async function realPreRegister (params = {}) {
const {
user
} = params
const {
userMatched
} = await findUser({
userQuery: user,
authorizedApp: this.getUniversalClientInfo().appId
})
if (userMatched.length > 0) {
throw {
errCode: ERROR.ACCOUNT_EXISTS
}
}
}
async function preRegister (params = {}) {
try {
await realPreRegister.call(this, params)
} catch (error) {
await this.middleware.uniIdLog({
success: false,
type: LOG_TYPE.REGISTER
})
throw error
}
}
async function preRegisterWithPassword (params = {}) {
const {
user,
password
} = params
await preRegister.call(this, {
user
})
const passwordUtils = new PasswordUtils({
clientInfo: this.getUniversalClientInfo(),
passwordSecret: this.config.passwordSecret
})
const {
passwordHash,
version
} = passwordUtils.generatePasswordHash({
password
})
const extraData = {
password: passwordHash,
password_secret_version: version
}
return {
user,
extraData
}
}
async function thirdPartyRegister ({
user = {}
} = {}) {
return {
mobileConfirmed: !!(user.mobile && user.mobile_confirmed) || false,
emailConfirmed: !!(user.email && user.email_confirmed) || false
}
}
async function postRegister (params = {}) {
const {
user,
extraData = {},
isThirdParty = false,
inviteCode
} = params
const {
appId,
appName,
appVersion,
appVersionCode,
channel,
scene,
clientIP,
osName
} = this.getUniversalClientInfo()
const uniIdToken = this.getUniversalUniIdToken()
merge(user, extraData)
const registerChannel = channel || scene
user.register_env = {
appid: appId || '',
uni_platform: this.clientPlatform || '',
os_name: osName || '',
app_name: appName || '',
app_version: appVersion || '',
app_version_code: appVersionCode || '',
channel: registerChannel ? registerChannel + '' : '', // channel可能为数字统一存为字符串
client_ip: clientIP || ''
}
user.register_date = Date.now()
user.dcloud_appid = [appId]
if (user.username) {
user.username = user.username.toLowerCase()
}
if (user.email) {
user.email = user.email.toLowerCase()
}
const {
autoSetInviteCode, // 注册时自动设置邀请码
forceInviteCode, // 必须有邀请码才允许注册注意此逻辑不可对admin生效
userRegisterDefaultRole // 用户注册时配置的默认角色
} = this.config
if (autoSetInviteCode) {
user.my_invite_code = await getValidInviteCode()
}
// 如果用户注册默认角色配置存在且不为空数组
if (userRegisterDefaultRole && userRegisterDefaultRole.length) {
// 将用户已有的角色和配置的默认角色合并成一个数组,并去重
user.role = Array.from(new Set([...(user.role || []), ...userRegisterDefaultRole]))
}
const isAdmin = user.role && user.role.includes('admin')
if (forceInviteCode && !isAdmin && !inviteCode) {
throw {
errCode: ERROR.INVALID_INVITE_CODE
}
}
if (inviteCode) {
const {
inviterUid,
inviteTime
} = await generateInviteInfo({
inviteCode
})
user.inviter_uid = inviterUid
user.invite_time = inviteTime
}
if (uniIdToken) {
try {
await logout.call(this)
} catch (error) { }
}
const beforeRegister = this.hooks.beforeRegister
let userRecord = user
if (beforeRegister) {
userRecord = await beforeRegister({
userRecord,
clientInfo: this.getUniversalClientInfo()
})
}
const {
id: uid
} = await userCollection.add(userRecord)
const createTokenRes = await this.uniIdCommon.createToken({
uid
})
const {
errCode,
token,
tokenExpired
} = createTokenRes
if (errCode) {
throw createTokenRes
}
await this.middleware.uniIdLog({
data: {
user_id: uid
},
type: LOG_TYPE.REGISTER
})
return {
errCode: 0,
uid,
newToken: {
token,
tokenExpired
},
...(
isThirdParty
? thirdPartyRegister({
user: {
...userRecord,
_id: uid
}
})
: {}
),
passwordConfirmed: !!userRecord.password
}
}
module.exports = {
preRegister,
preRegisterWithPassword,
postRegister
}

View File

@ -0,0 +1,166 @@
const {
findUser
} = require('./account')
const {
ERROR
} = require('../../common/error')
const {
userCollection, dbCmd, USER_IDENTIFIER
} = require('../../common/constants')
const {
getUserIdentifier
} = require('../../lib/utils/account')
const {
batchFindObjctValue
} = require('../../common/utils')
const {
merge
} = require('../npm/index')
/**
*
* @param {object} param
* @param {string} param.uid 用户id
* @param {string} param.bindAccount 要绑定的三方账户、手机号或邮箱
*/
async function preBind ({
uid,
bindAccount,
logType
} = {}) {
const {
userMatched
} = await findUser({
userQuery: bindAccount,
authorizedApp: this.getUniversalClientInfo().appId
})
if (userMatched.length > 0) {
await this.middleware.uniIdLog({
data: {
user_id: uid
},
type: logType,
success: false
})
throw {
errCode: ERROR.BIND_CONFLICT
}
}
}
async function postBind ({
uid,
extraData = {},
bindAccount,
logType
} = {}) {
await userCollection.doc(uid).update(merge(bindAccount, extraData))
await this.middleware.uniIdLog({
data: {
user_id: uid
},
type: logType
})
return {
errCode: 0
}
}
async function preUnBind ({
uid,
unBindAccount,
logType
}) {
const notUnBind = ['username', 'mobile', 'email']
const userIdentifier = getUserIdentifier(unBindAccount)
const condition = Object.keys(userIdentifier).reduce((res, key) => {
if (userIdentifier[key]) {
if (notUnBind.includes(key)) {
throw {
errCode: ERROR.UNBIND_NOT_SUPPORTED
}
}
res.push({
[key]: userIdentifier[key]
})
}
return res
}, [])
const currentUnBindAccount = Object.keys(userIdentifier).reduce((res, key) => {
if (userIdentifier[key]) {
res.push(key)
}
return res
}, [])
const { data: users } = await userCollection.where(dbCmd.and(
{ _id: uid },
dbCmd.or(condition)
)).get()
if (users.length <= 0) {
await this.middleware.uniIdLog({
data: {
user_id: uid
},
type: logType,
success: false
})
throw {
errCode: ERROR.UNBIND_FAIL
}
}
const [user] = users
const otherAccounts = batchFindObjctValue(user, Object.keys(USER_IDENTIFIER).filter(key => !notUnBind.includes(key) && !currentUnBindAccount.includes(key)))
let hasOtherAccountBind = false
for (const key in otherAccounts) {
if (otherAccounts[key]) {
hasOtherAccountBind = true
break
}
}
// 如果没有其他第三方登录方式
if (!hasOtherAccountBind) {
// 存在用户名或者邮箱但是没有设置过没密码就提示设置密码
if ((user.username || user.email) && !user.password) {
throw {
errCode: ERROR.UNBIND_PASSWORD_NOT_EXISTS
}
}
// 账号任何登录方式都没有就优先绑定手机号
if (!user.mobile) {
throw {
errCode: ERROR.UNBIND_MOBILE_NOT_EXISTS
}
}
}
}
async function postUnBind ({
uid,
unBindAccount,
logType
}) {
await userCollection.doc(uid).update(unBindAccount)
await this.middleware.uniIdLog({
data: {
user_id: uid
},
type: logType
})
return {
errCode: 0
}
}
module.exports = {
preBind,
postBind,
preUnBind,
postUnBind
}

View File

@ -0,0 +1,79 @@
const {
setMobileVerifyCode
} = require('./verify-code')
const {
getVerifyCode
} = require('../../common/utils')
/**
* 发送短信
* @param {object} param
* @param {string} param.mobile 手机号
* @param {object} param.code 可选,验证码
* @param {object} param.scene 短信场景
* @param {object} param.templateId 可选短信模板id
* @returns
*/
async function sendSmsCode ({
mobile,
code,
scene,
templateId
} = {}) {
const requiredParams = [
'name',
'codeExpiresIn'
]
const smsConfig = (this.config.service && this.config.service.sms) || {}
for (let i = 0; i < requiredParams.length; i++) {
const key = requiredParams[i]
if (!smsConfig[key]) {
throw new Error(`Missing config param: service.sms.${key}`)
}
}
if (!code) {
code = getVerifyCode()
}
let action
switch (scene) {
case 'login-by-sms':
action = this.t('login')
break
default:
action = this.t('verify-mobile')
break
}
const sceneConfig = (smsConfig.scene || {})[scene] || {}
if (!templateId) {
templateId = sceneConfig.templateId
}
if (!templateId) {
throw new Error('"templateId" is required')
}
const codeExpiresIn = sceneConfig.codeExpiresIn || smsConfig.codeExpiresIn
await setMobileVerifyCode.call(this, {
mobile,
code,
expiresIn: codeExpiresIn,
scene
})
await uniCloud.sendSms({
smsKey: smsConfig.smsKey,
smsSecret: smsConfig.smsSecret,
phone: mobile,
templateId,
data: {
name: smsConfig.name,
code,
action,
expMinute: '' + Math.round(codeExpiresIn / 60)
}
})
return {
errCode: 0
}
}
module.exports = {
sendSmsCode
}

View File

@ -0,0 +1,106 @@
const {
checkLoginUserRecord,
postLogin
} = require('./login')
const {
postRegister
} = require('./register')
const {
findUser
} = require('./account')
const {
ERROR
} = require('../../common/error')
async function realPreUnifiedLogin (params = {}) {
const {
user,
type
} = params
const appId = this.getUniversalClientInfo().appId
const {
total,
userMatched
} = await findUser({
userQuery: user,
authorizedApp: appId
})
if (userMatched.length === 0) {
if (type === 'login') {
if (total > 0) {
throw {
errCode: ERROR.ACCOUNT_NOT_EXISTS_IN_CURRENT_APP
}
}
throw {
errCode: ERROR.ACCOUNT_NOT_EXISTS
}
}
return {
type: 'register',
user
}
} if (userMatched.length === 1) {
if (type === 'register') {
throw {
errCode: ERROR.ACCOUNT_EXISTS
}
}
const userRecord = userMatched[0]
checkLoginUserRecord(userRecord)
return {
type: 'login',
user: userRecord
}
} else if (userMatched.length > 1) {
throw {
errCode: ERROR.ACCOUNT_CONFLICT
}
}
}
async function preUnifiedLogin (params = {}) {
try {
const result = await realPreUnifiedLogin.call(this, params)
return result
} catch (error) {
await this.middleware.uniIdLog({
success: false
})
throw error
}
}
async function postUnifiedLogin (params = {}) {
const {
user,
extraData = {},
isThirdParty = false,
type,
inviteCode
} = params
let result
if (type === 'login') {
result = await postLogin.call(this, {
user,
extraData,
isThirdParty
})
} else if (type === 'register') {
result = await postRegister.call(this, {
user,
extraData,
isThirdParty,
inviteCode
})
}
return {
...result,
type
}
}
module.exports = {
preUnifiedLogin,
postUnifiedLogin
}

View File

@ -0,0 +1,27 @@
async function getPhoneNumber ({
// eslint-disable-next-line camelcase
access_token,
openid
} = {}) {
const requiredParams = []
const univerifyConfig = (this.config.service && this.config.service.univerify) || {}
for (let i = 0; i < requiredParams.length; i++) {
const key = requiredParams[i]
if (!univerifyConfig[key]) {
throw new Error(`Missing config param: service.univerify.${key}`)
}
}
return uniCloud.getPhoneNumber({
provider: 'univerify',
appid: this.getUniversalClientInfo().appId,
apiKey: univerifyConfig.apiKey,
apiSecret: univerifyConfig.apiSecret,
// eslint-disable-next-line camelcase
access_token,
openid
})
}
module.exports = {
getPhoneNumber
}

View File

@ -0,0 +1,25 @@
const {
userCollection
} = require('../../common/constants')
const {
USER_STATUS
} = require('../../common/constants')
async function setUserStatus (uid, status) {
const updateData = {
status
}
if (status !== USER_STATUS.NORMAL) {
updateData.valid_token_date = Date.now()
}
await userCollection.doc(uid).update({
status
})
// TODO 此接口尚不完善例如注销后其他客户端可能存在有效token支持Redis后此处会补充额外逻辑
return {
errCode: 0
}
}
module.exports = {
setUserStatus
}

View File

@ -0,0 +1,18 @@
let redisEnable = null
function getRedisEnable() {
// 未用到的时候不调用redis接口节省一些连接数
if (redisEnable !== null) {
return redisEnable
}
try {
uniCloud.redis()
redisEnable = true
} catch (error) {
redisEnable = false
}
return redisEnable
}
module.exports = {
getRedisEnable
}

View File

@ -0,0 +1,152 @@
const {
dbCmd,
verifyCollection
} = require('../../common/constants')
const {
ERROR
} = require('../../common/error')
const {
getVerifyCode
} = require('../../common/utils')
async function setVerifyCode ({
mobile,
email,
code,
expiresIn,
scene
} = {}) {
const now = Date.now()
const record = {
mobile,
email,
scene,
code: code || getVerifyCode(),
state: 0,
ip: this.getUniversalClientInfo().clientIP,
created_date: now,
expired_date: now + expiresIn * 1000
}
await verifyCollection.add(record)
return {
errCode: 0
}
}
async function setEmailVerifyCode ({
email,
code,
expiresIn,
scene
} = {}) {
email = email && email.trim()
if (!email) {
throw {
errCode: ERROR.INVALID_EMAIL
}
}
email = email.toLowerCase()
return setVerifyCode.call(this, {
email,
code,
expiresIn,
scene
})
}
async function setMobileVerifyCode ({
mobile,
code,
expiresIn,
scene
} = {}) {
mobile = mobile && mobile.trim()
if (!mobile) {
throw {
errCode: ERROR.INVALID_MOBILE
}
}
return setVerifyCode.call(this, {
mobile,
code,
expiresIn,
scene
})
}
async function verifyEmailCode ({
email,
code,
scene
} = {}) {
email = email && email.trim()
if (!email) {
throw {
errCode: ERROR.INVALID_EMAIL
}
}
email = email.toLowerCase()
const {
data: codeRecord
} = await verifyCollection.where({
email,
scene,
code,
state: 0,
expired_date: dbCmd.gt(Date.now())
}).limit(1).get()
if (codeRecord.length === 0) {
throw {
errCode: ERROR.EMAIL_VERIFY_CODE_ERROR
}
}
await verifyCollection.doc(codeRecord[0]._id).update({
state: 1
})
return {
errCode: 0
}
}
async function verifyMobileCode ({
mobile,
code,
scene
} = {}) {
mobile = mobile && mobile.trim()
if (!mobile) {
throw {
errCode: ERROR.INVALID_MOBILE
}
}
const {
data: codeRecord
} = await verifyCollection.where({
mobile,
scene,
code,
state: 0,
expired_date: dbCmd.gt(Date.now())
}).limit(1).get()
if (codeRecord.length === 0) {
throw {
errCode: ERROR.MOBILE_VERIFY_CODE_ERROR
}
}
await verifyCollection.doc(codeRecord[0]._id).update({
state: 1
})
return {
errCode: 0
}
}
module.exports = {
verifyEmailCode,
verifyMobileCode,
setEmailVerifyCode,
setMobileVerifyCode
}

View File

@ -0,0 +1,236 @@
const crypto = require('crypto')
const {
userCollection
} = require('../../common/constants')
const {
ERROR
} = require('../../common/error')
const {
getRedisEnable
} = require('./utils')
const {
openDataCollection
} = require('../../common/constants')
function decryptWeixinData ({
encryptedData,
sessionKey,
iv
} = {}) {
const oauthConfig = this.configUtils.getOauthConfig({
provider: 'weixin'
})
const decipher = crypto.createDecipheriv(
'aes-128-cbc',
Buffer.from(sessionKey, 'base64'),
Buffer.from(iv, 'base64')
)
// 设置自动 padding 为 true删除填充补位
decipher.setAutoPadding(true)
let decoded
decoded = decipher.update(encryptedData, 'base64', 'utf8')
decoded += decipher.final('utf8')
decoded = JSON.parse(decoded)
if (decoded.watermark.appid !== oauthConfig.appid) {
throw new Error('Invalid wechat appid in decode content')
}
return decoded
}
function getWeixinPlatform () {
const platform = this.clientPlatform
const userAgent = this.getUniversalClientInfo().userAgent
switch (platform) {
case 'app':
case 'app-plus':
case 'app-android':
case 'app-ios':
return 'app'
case 'mp-weixin':
return 'mp'
case 'h5':
case 'web':
return userAgent.indexOf('MicroMessenger') > -1 ? 'h5' : 'web'
default:
throw new Error('Unsupported weixin platform')
}
}
async function saveWeixinUserKey ({
openid,
sessionKey, // 微信小程序用户sessionKey
accessToken, // App端微信用户accessToken
refreshToken, // App端微信用户refreshToken
accessTokenExpired // App端微信用户accessToken过期时间
} = {}) {
// 微信公众平台、开放平台refreshToken有效期均为30天微信没有在网络请求里面返回30天这个值务必注意未来可能出现调整需及时更新此处逻辑
// 此前QQ开放平台有调整过accessToken的过期时间[access_token有效期由90天缩短至30天](https://wiki.connect.qq.com/%E3%80%90qq%E4%BA%92%E8%81%94%E3%80%91access_token%E6%9C%89%E6%95%88%E6%9C%9F%E8%B0%83%E6%95%B4)
const appId = this.getUniversalClientInfo().appId
const weixinPlatform = getWeixinPlatform.call(this)
const keyObj = {
dcloudAppid: appId,
openid,
platform: 'weixin-' + weixinPlatform
}
switch (weixinPlatform) {
case 'mp':
await this.uniOpenBridge.setSessionKey(keyObj, {
session_key: sessionKey
}, 30 * 24 * 60 * 60)
break
case 'app':
case 'h5':
case 'web':
await this.uniOpenBridge.setUserAccessToken(keyObj, {
access_token: accessToken,
refresh_token: refreshToken,
access_token_expired: accessTokenExpired
}, 30 * 24 * 60 * 60)
break
default:
break
}
}
async function saveSecureNetworkCache ({
code,
openid,
unionid,
sessionKey
}) {
const {
appId
} = this.getUniversalClientInfo()
const key = `uni-id:${appId}:weixin-mp:code:${code}:secure-network-cache`
const value = JSON.stringify({
openid,
unionid,
session_key: sessionKey
})
// 此处存储的是code的缓存设置有效期和token一致
const expiredSeconds = this.config.tokenExpiresIn || 3 * 24 * 60 * 60
await openDataCollection.doc(key).set({
value,
expired: Date.now() + expiredSeconds * 1000
})
const isRedisEnable = getRedisEnable()
if (isRedisEnable) {
const redis = uniCloud.redis()
await redis.set(key, value, 'EX', expiredSeconds)
}
}
function generateWeixinCache ({
sessionKey, // 微信小程序用户sessionKey
accessToken, // App端微信用户accessToken
refreshToken, // App端微信用户refreshToken
accessTokenExpired // App端微信用户accessToken过期时间
} = {}) {
const platform = getWeixinPlatform.call(this)
let cache
switch (platform) {
case 'app':
case 'h5':
case 'web':
cache = {
access_token: accessToken,
refresh_token: refreshToken,
access_token_expired: accessTokenExpired
}
break
case 'mp':
cache = {
session_key: sessionKey
}
break
default:
throw new Error('Unsupported weixin platform')
}
return {
third_party: {
[`${platform}_weixin`]: cache
}
}
}
function getWeixinOpenid ({
userRecord
} = {}) {
const weixinPlatform = getWeixinPlatform.call(this)
const appId = this.getUniversalClientInfo().appId
const wxOpenidObj = userRecord.wx_openid
if (!wxOpenidObj) {
return
}
return wxOpenidObj[`${weixinPlatform}_${appId}`] || wxOpenidObj[weixinPlatform]
}
async function getWeixinCacheFallback ({
userRecord,
key
} = {}) {
const platform = getWeixinPlatform.call(this)
const thirdParty = userRecord && userRecord.third_party
if (!thirdParty) {
return
}
const weixinCache = thirdParty[`${platform}_weixin`]
return weixinCache && weixinCache[key]
}
async function getWeixinCache ({
uid,
userRecord,
key
} = {}) {
const weixinPlatform = getWeixinPlatform.call(this)
const appId = this.getUniversalClientInfo().appId
if (!userRecord) {
const getUserRes = await userCollection.doc(uid).get()
userRecord = getUserRes.data[0]
}
if (!userRecord) {
throw {
errCode: ERROR.ACCOUNT_NOT_EXISTS
}
}
const openid = getWeixinOpenid.call(this, {
userRecord
})
const getCacheMethod = weixinPlatform === 'mp' ? 'getSessionKey' : 'getUserAccessToken'
const userKey = await this.uniOpenBridge[getCacheMethod]({
dcloudAppid: appId,
platform: 'weixin-' + weixinPlatform,
openid
})
if (userKey) {
return userKey[key]
}
return getWeixinCacheFallback({
userRecord,
key
})
}
async function getWeixinAccessToken () {
const weixinPlatform = getWeixinPlatform.call(this)
const appId = this.getUniversalClientInfo().appId
const cache = await this.uniOpenBridge.getAccessToken({
dcloudAppid: appId,
platform: 'weixin-' + weixinPlatform
})
return cache.access_token
}
module.exports = {
decryptWeixinData,
getWeixinPlatform,
generateWeixinCache,
getWeixinCache,
saveWeixinUserKey,
getWeixinAccessToken,
saveSecureNetworkCache
}

View File

@ -0,0 +1,59 @@
const methodPermission = require('../config/permission')
const {
ERROR
} = require('../common/error')
function isAccessAllowed (user, setting) {
const {
role: userRole = [],
permission: userPermission = []
} = user
const {
role: settingRole = [],
permission: settingPermission = []
} = setting
if (userRole.includes('admin')) {
return
}
if (
settingRole.length > 0 &&
settingRole.every(item => !userRole.includes(item))
) {
throw {
errCode: ERROR.PERMISSION_ERROR
}
}
if (
settingPermission.length > 0 &&
settingPermission.every(item => !userPermission.includes(item))
) {
throw {
errCode: ERROR.PERMISSION_ERROR
}
}
}
module.exports = async function () {
const methodName = this.getMethodName()
if (!(methodName in methodPermission)) {
return
}
const {
auth,
role,
permission
} = methodPermission[methodName]
if (auth || role || permission) {
await this.middleware.auth()
}
if (role && role.length === 0) {
throw new Error('[AccessControl]Empty role array is not supported')
}
if (permission && permission.length === 0) {
throw new Error('[AccessControl]Empty permission array is not supported')
}
return isAccessAllowed(this.authInfo, {
role,
permission
})
}

View File

@ -0,0 +1,17 @@
module.exports = async function () {
if (this.authInfo) { // 多次执行auth时如果第一次成功后续不再执行
return
}
const token = this.getUniversalUniIdToken()
const payload = await this.uniIdCommon.checkToken(token)
if (payload.errCode) {
throw payload
}
this.authInfo = payload
if (payload.token) {
this.response.newToken = {
token: payload.token,
tokenExpired: payload.tokenExpired
}
}
}

View File

@ -0,0 +1,8 @@
module.exports = {
auth: require('./auth'),
uniIdLog: require('./uni-id-log'),
validate: require('./validate'),
accessControl: require('./access-control'),
verifyRequestSign: require('./verify-request-sign'),
...require('./rbac')
}

View File

@ -0,0 +1,39 @@
const {
ERROR
} = require('../common/error')
function hasRole (...roleList) {
const userRole = this.authInfo.role || []
if (userRole.includes('admin')) {
return
}
const isMatch = roleList.every(roleItem => {
return userRole.includes(roleItem)
})
if (!isMatch) {
throw {
errCode: ERROR.PERMISSION_ERROR
}
}
}
function hasPermission (...permissionList) {
const userRole = this.authInfo.role || []
const userPermission = this.authInfo.permission || []
if (userRole.includes('admin')) {
return
}
const isMatch = permissionList.every(permissionItem => {
return userPermission.includes(permissionItem)
})
if (!isMatch) {
throw {
errCode: ERROR.PERMISSION_ERROR
}
}
}
module.exports = {
hasRole,
hasPermission
}

View File

@ -0,0 +1,39 @@
const db = uniCloud.database()
module.exports = async function ({
data = {},
success = true,
type = 'login'
} = {}) {
const now = Date.now()
const uniIdLogCollection = db.collection('uni-id-log')
const requiredDataKeyList = ['user_id', 'username', 'email', 'mobile']
const dataCopy = {}
for (let i = 0; i < requiredDataKeyList.length; i++) {
const key = requiredDataKeyList[i]
if (key in data && typeof data[key] === 'string') {
dataCopy[key] = data[key]
}
}
const {
appId,
clientIP,
deviceId,
userAgent
} = this.getUniversalClientInfo()
const logData = {
appid: appId,
device_id: deviceId,
ip: clientIP,
type,
ua: userAgent,
create_date: now,
...dataCopy
}
if (success) {
logData.state = 1
} else {
logData.state = 0
}
return uniIdLogCollection.add(logData)
}

View File

@ -0,0 +1,7 @@
module.exports = function (value = {}, schema = {}) {
const validateRes = this.validator.validate(value, schema)
if (validateRes) {
delete validateRes.schemaKey
throw validateRes
}
}

View File

@ -0,0 +1,85 @@
const crypto = require('crypto')
const createConfig = require('uni-config-center')
const { verifyHttpInfo } = require('uni-cloud-s2s')
const { ERROR } = require('../common/error')
const s2sConfig = createConfig({
pluginId: 'uni-cloud-s2s'
})
const needSignFunctions = new Set([
'externalRegister',
'externalLogin',
'updateUserInfoByExternal'
])
module.exports = function () {
const methodName = this.getMethodName()
const { source } = this.getUniversalClientInfo()
// 指定接口需要鉴权
if (!needSignFunctions.has(methodName)) return
// 非 HTTP 方式请求拒绝访问
if (source !== 'http') {
throw {
errCode: ERROR.ILLEGAL_REQUEST
}
}
// 支持 uni-cloud-s2s 验证请求
if (s2sConfig.hasFile('config.json')) {
try {
if (!verifyHttpInfo(this.getHttpInfo())) {
throw {
errCode: ERROR.ILLEGAL_REQUEST
}
}
} catch (e) {
if (e.errSubject === 'uni-cloud-s2s') {
throw {
errCode: ERROR.ILLEGAL_REQUEST,
errMsg: e.errMsg
}
}
throw e
}
return
}
if (!this.config.requestAuthSecret || typeof this.config.requestAuthSecret !== 'string') {
throw {
errCode: ERROR.CONFIG_FIELD_REQUIRED,
errMsgValue: {
field: 'requestAuthSecret'
}
}
}
const timeout = 20 * 1000 // 请求超过20秒不能再请求防止重放攻击
const { headers, body: _body } = this.getHttpInfo()
const { 'uni-id-nonce': nonce, 'uni-id-timestamp': timestamp, 'uni-id-signature': signature } = headers
const body = JSON.parse(_body).params || {}
const bodyStr = Object.keys(body)
.sort()
.filter(item => typeof body[item] !== 'object')
.map(item => `${item}=${body[item]}`)
.join('&')
if (isNaN(Number(timestamp)) || (Number(timestamp) + timeout) < Date.now()) {
console.error('[timestamp error], timestamp:', timestamp, 'timeout:', timeout)
throw {
errCode: ERROR.ILLEGAL_REQUEST
}
}
const reSignature = crypto.createHmac('sha256', `${this.config.requestAuthSecret + nonce}`).update(`${timestamp}${bodyStr}`).digest('hex')
if (signature !== reSignature.toUpperCase()) {
console.error('[signature error], signature:', signature, 'reSignature:', reSignature.toUpperCase(), 'requestAuthSecret:', this.config.requestAuthSecret)
throw {
errCode: ERROR.ILLEGAL_REQUEST
}
}
}

View File

@ -0,0 +1,16 @@
const {
setUserStatus
} = require('../../lib/utils/update-user-info')
const {
USER_STATUS
} = require('../../common/constants')
/**
* 注销账户
* @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#close-account
* @returns
*/
module.exports = async function () {
const { uid } = this.authInfo
return setUserStatus(uid, USER_STATUS.CLOSED)
}

View File

@ -0,0 +1,69 @@
const {
userCollection
} = require('../../common/constants')
const {
ERROR
} = require('../../common/error')
function isUsernameSet (userRecord) {
return !!userRecord.username
}
function isNicknameSet (userRecord) {
return !!userRecord.nickname
}
function isPasswordSet (userRecord) {
return !!userRecord.password
}
function isMobileBound (userRecord) {
return !!(userRecord.mobile && userRecord.mobile_confirmed)
}
function isEmailBound (userRecord) {
return !!(userRecord.email && userRecord.email_confirmed)
}
function isWeixinBound (userRecord) {
return !!(
userRecord.wx_unionid ||
Object.keys(userRecord.wx_openid || {}).length
)
}
function isQQBound (userRecord) {
return !!(
userRecord.qq_unionid ||
Object.keys(userRecord.qq_openid || {}).length
)
}
function isAlipayBound (userRecord) {
return !!userRecord.ali_openid
}
function isAppleBound (userRecord) {
return !!userRecord.apple_openid
}
/**
* 获取账户账户简略信息
* @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#get-account-info
*/
module.exports = async function () {
const {
uid
} = this.authInfo
const getUserRes = await userCollection.doc(uid).get()
const userRecord = getUserRes && getUserRes.data && getUserRes.data[0]
if (!userRecord) {
throw {
errCode: ERROR.ACCOUNT_NOT_EXISTS
}
}
return {
errCode: 0,
isUsernameSet: isUsernameSet(userRecord),
isNicknameSet: isNicknameSet(userRecord),
isPasswordSet: isPasswordSet(userRecord),
isMobileBound: isMobileBound(userRecord),
isEmailBound: isEmailBound(userRecord),
isWeixinBound: isWeixinBound(userRecord),
isQQBound: isQQBound(userRecord),
isAlipayBound: isAlipayBound(userRecord),
isAppleBound: isAppleBound(userRecord)
}
}

View File

@ -0,0 +1,45 @@
const { userCollection } = require('../../common/constants')
const { ERROR } = require('../../common/error')
const { decryptData } = require('../../common/sensitive-aes-cipher')
const { dataDesensitization } = require('../../common/utils')
/**
* 获取实名信息
* @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#get-realname-info
* @param {Object} params
* @param {Boolean} params.decryptData 是否解密数据
* @returns
*/
module.exports = async function (params = {}) {
const schema = {
decryptData: {
required: false,
type: 'boolean'
}
}
this.middleware.validate(params, schema)
const { decryptData: isDecryptData = true } = params
const {
uid
} = this.authInfo
const getUserRes = await userCollection.doc(uid).get()
const userRecord = getUserRes && getUserRes.data && getUserRes.data[0]
if (!userRecord) {
throw {
errCode: ERROR.ACCOUNT_NOT_EXISTS
}
}
const { realname_auth: realNameAuth = {} } = userRecord
return {
errCode: 0,
type: realNameAuth.type,
authStatus: realNameAuth.auth_status,
realName: isDecryptData ? dataDesensitization(decryptData.call(this, realNameAuth.real_name), { onlyLast: true }) : realNameAuth.real_name,
identity: isDecryptData ? dataDesensitization(decryptData.call(this, realNameAuth.identity)) : realNameAuth.identity
}
}

View File

@ -0,0 +1,9 @@
module.exports = {
setPwd: require('./set-pwd'),
updatePwd: require('./update-pwd'),
resetPwdBySms: require('./reset-pwd-by-sms'),
resetPwdByEmail: require('./reset-pwd-by-email'),
closeAccount: require('./close-account'),
getAccountInfo: require('./get-account-info'),
getRealNameInfo: require('./get-realname-info')
}

View File

@ -0,0 +1,128 @@
const {
ERROR
} = require('../../common/error')
const {
getNeedCaptcha,
verifyCaptcha
} = require('../../lib/utils/captcha')
const {
verifyEmailCode
} = require('../../lib/utils/verify-code')
const {
userCollection,
EMAIL_SCENE,
CAPTCHA_SCENE,
LOG_TYPE
} = require('../../common/constants')
const {
findUser
} = require('../../lib/utils/account')
const PasswordUtils = require('../../lib/utils/password')
/**
* 通过邮箱验证码重置密码
* @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#reset-pwd-by-email
* @param {object} params
* @param {string} params.email 邮箱
* @param {string} params.code 邮箱验证码
* @param {string} params.password 密码
* @param {string} params.captcha 图形验证码
* @returns {object}
*/
module.exports = async function (params = {}) {
const schema = {
email: 'email',
code: 'string',
password: 'password',
captcha: {
required: false,
type: 'string'
}
}
this.middleware.validate(params, schema)
const {
email,
code,
password,
captcha
} = params
const needCaptcha = await getNeedCaptcha.call(this, {
email,
type: LOG_TYPE.RESET_PWD_BY_EMAIL
})
if (needCaptcha) {
await verifyCaptcha.call(this, {
captcha,
scene: CAPTCHA_SCENE.RESET_PWD_BY_EMAIL
})
}
try {
// 验证手机号验证码,验证不通过时写入失败日志
await verifyEmailCode({
email,
code,
scene: EMAIL_SCENE.RESET_PWD_BY_EMAIL
})
} catch (error) {
await this.middleware.uniIdLog({
data: {
email
},
type: LOG_TYPE.RESET_PWD_BY_EMAIL,
success: false
})
throw error
}
// 根据手机号查找匹配的用户
const {
total,
userMatched
} = await findUser.call(this, {
userQuery: {
email
},
authorizedApp: [this.getUniversalClientInfo().appId]
})
if (userMatched.length === 0) {
if (total > 0) {
throw {
errCode: ERROR.ACCOUNT_NOT_EXISTS_IN_CURRENT_APP
}
}
throw {
errCode: ERROR.ACCOUNT_NOT_EXISTS
}
} else if (userMatched.length > 1) {
throw {
errCode: ERROR.ACCOUNT_CONFLICT
}
}
const { _id: uid } = userMatched[0]
const {
passwordHash,
version
} = new PasswordUtils({
clientInfo: this.getUniversalClientInfo(),
passwordSecret: this.config.passwordSecret
}).generatePasswordHash({
password
})
// 更新用户密码
await userCollection.doc(uid).update({
password: passwordHash,
password_secret_version: version,
valid_token_date: Date.now()
})
// 写入成功日志
await this.middleware.uniIdLog({
data: {
email
},
type: LOG_TYPE.RESET_PWD_BY_SMS
})
return {
errCode: 0
}
}

View File

@ -0,0 +1,128 @@
const {
ERROR
} = require('../../common/error')
const {
getNeedCaptcha,
verifyCaptcha
} = require('../../lib/utils/captcha')
const {
verifyMobileCode
} = require('../../lib/utils/verify-code')
const {
userCollection,
SMS_SCENE,
CAPTCHA_SCENE,
LOG_TYPE
} = require('../../common/constants')
const {
findUser
} = require('../../lib/utils/account')
const PasswordUtils = require('../../lib/utils/password')
/**
* 通过短信验证码重置密码
* @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#reset-pwd-by-sms
* @param {object} params
* @param {string} params.mobile 手机号
* @param {string} params.mobile 短信验证码
* @param {string} params.password 密码
* @param {string} params.captcha 图形验证码
* @returns {object}
*/
module.exports = async function (params = {}) {
const schema = {
mobile: 'mobile',
code: 'string',
password: 'password',
captcha: {
required: false,
type: 'string'
}
}
this.middleware.validate(params, schema)
const {
mobile,
code,
password,
captcha
} = params
const needCaptcha = await getNeedCaptcha.call(this, {
mobile,
type: LOG_TYPE.RESET_PWD_BY_SMS
})
if (needCaptcha) {
await verifyCaptcha.call(this, {
captcha,
scene: CAPTCHA_SCENE.RESET_PWD_BY_SMS
})
}
try {
// 验证手机号验证码,验证不通过时写入失败日志
await verifyMobileCode({
mobile,
code,
scene: SMS_SCENE.RESET_PWD_BY_SMS
})
} catch (error) {
await this.middleware.uniIdLog({
data: {
mobile
},
type: LOG_TYPE.RESET_PWD_BY_SMS,
success: false
})
throw error
}
// 根据手机号查找匹配的用户
const {
total,
userMatched
} = await findUser.call(this, {
userQuery: {
mobile
},
authorizedApp: [this.getUniversalClientInfo().appId]
})
if (userMatched.length === 0) {
if (total > 0) {
throw {
errCode: ERROR.ACCOUNT_NOT_EXISTS_IN_CURRENT_APP
}
}
throw {
errCode: ERROR.ACCOUNT_NOT_EXISTS
}
} else if (userMatched.length > 1) {
throw {
errCode: ERROR.ACCOUNT_CONFLICT
}
}
const { _id: uid } = userMatched[0]
const {
passwordHash,
version
} = new PasswordUtils({
clientInfo: this.getUniversalClientInfo(),
passwordSecret: this.config.passwordSecret
}).generatePasswordHash({
password
})
// 更新用户密码
await userCollection.doc(uid).update({
password: passwordHash,
password_secret_version: version,
valid_token_date: Date.now()
})
// 写入成功日志
await this.middleware.uniIdLog({
data: {
mobile
},
type: LOG_TYPE.RESET_PWD_BY_SMS
})
return {
errCode: 0
}
}

View File

@ -0,0 +1,83 @@
const { userCollection, SMS_SCENE, LOG_TYPE, CAPTCHA_SCENE } = require('../../common/constants')
const { ERROR } = require('../../common/error')
const { verifyMobileCode } = require('../../lib/utils/verify-code')
const PasswordUtils = require('../../lib/utils/password')
const { getNeedCaptcha, verifyCaptcha } = require('../../lib/utils/captcha')
module.exports = async function (params = {}) {
const schema = {
password: 'password',
code: 'string',
captcha: {
required: false,
type: 'string'
}
}
this.middleware.validate(params, schema)
const { password, code, captcha } = params
const uid = this.authInfo.uid
const getUserRes = await userCollection.doc(uid).get()
const userRecord = getUserRes.data[0]
if (!userRecord) {
throw {
errCode: ERROR.ACCOUNT_NOT_EXISTS
}
}
const needCaptcha = await getNeedCaptcha.call(this, {
mobile: userRecord.mobile
})
if (needCaptcha) {
await verifyCaptcha.call(this, {
captcha,
scene: CAPTCHA_SCENE.SET_PWD_BY_SMS
})
}
try {
// 验证手机号验证码,验证不通过时写入失败日志
await verifyMobileCode({
mobile: userRecord.mobile,
code,
scene: SMS_SCENE.SET_PWD_BY_SMS
})
} catch (error) {
await this.middleware.uniIdLog({
data: {
mobile: userRecord.mobile
},
type: LOG_TYPE.SET_PWD_BY_SMS,
success: false
})
throw error
}
const {
passwordHash,
version
} = new PasswordUtils({
clientInfo: this.getUniversalClientInfo(),
passwordSecret: this.config.passwordSecret
}).generatePasswordHash({
password
})
// 更新用户密码
await userCollection.doc(uid).update({
password: passwordHash,
password_secret_version: version
})
await this.middleware.uniIdLog({
data: {
mobile: userRecord.mobile
},
type: LOG_TYPE.SET_PWD_BY_SMS
})
return {
errCode: 0
}
}

View File

@ -0,0 +1,69 @@
const {
userCollection
} = require('../../common/constants')
const {
ERROR
} = require('../../common/error')
const PasswordUtils = require('../../lib/utils/password')
/**
* 更新密码
* @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#update-pwd
* @param {object} params
* @param {string} params.oldPassword 旧密码
* @param {string} params.newPassword 新密码
* @returns {object}
*/
module.exports = async function (params = {}) {
const schema = {
oldPassword: 'string', // 防止密码规则调整导致旧密码无法更新
newPassword: 'password'
}
this.middleware.validate(params, schema)
const uid = this.authInfo.uid
const getUserRes = await userCollection.doc(uid).get()
const userRecord = getUserRes.data[0]
if (!userRecord) {
throw {
errCode: ERROR.ACCOUNT_NOT_EXISTS
}
}
const {
oldPassword,
newPassword
} = params
const passwordUtils = new PasswordUtils({
userRecord,
clientInfo: this.getUniversalClientInfo(),
passwordSecret: this.config.passwordSecret
})
const {
success: checkPasswordSuccess
} = passwordUtils.checkUserPassword({
password: oldPassword,
autoRefresh: false
})
if (!checkPasswordSuccess) {
throw {
errCode: ERROR.PASSWORD_ERROR
}
}
const {
passwordHash,
version
} = passwordUtils.generatePasswordHash({
password: newPassword
})
await userCollection.doc(uid).update({
password: passwordHash,
password_secret_version: version,
valid_token_date: Date.now() // refreshToken时会校验如果创建token时间在此时间点之前则拒绝下发新token返回token失效错误码
})
// 执行更新密码操作后客户端应将用户退出重新登录
return {
errCode: 0
}
}

View File

@ -0,0 +1,131 @@
const {
findUser
} = require('../../lib/utils/account')
const {
ERROR
} = require('../../common/error')
const {
userCollection
} = require('../../common/constants')
const PasswordUtils = require('../../lib/utils/password')
/**
* 新增用户
* @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#add-user
* @param {Object} params
* @param {String} params.username 用户名
* @param {String} params.password 密码
* @param {String} params.nickname 昵称
* @param {Array} params.authorizedApp 允许登录的AppID列表
* @param {Array} params.role 用户角色列表
* @param {String} params.mobile 手机号
* @param {String} params.email 邮箱
* @param {Array} params.tags 用户标签
* @param {Number} params.status 用户状态
* @returns
*/
module.exports = async function (params = {}) {
const schema = {
username: 'username',
password: 'password',
authorizedApp: {
required: false,
type: 'array<string>'
}, // 指定允许登录的app传空数组或不传时表示可以不可以在任何端登录
nickname: {
required: false,
type: 'nickname'
},
role: {
require: false,
type: 'array<string>'
},
mobile: {
required: false,
type: 'mobile'
},
email: {
required: false,
type: 'email'
},
tags: {
required: false,
type: 'array<string>'
},
status: {
required: false,
type: 'number'
}
}
this.middleware.validate(params, schema)
const {
username,
password,
authorizedApp,
nickname,
role,
mobile,
email,
tags,
status
} = params
const {
userMatched
} = await findUser({
userQuery: {
username,
mobile,
email
},
authorizedApp
})
if (userMatched.length) {
throw {
errCode: ERROR.ACCOUNT_EXISTS
}
}
const passwordUtils = new PasswordUtils({
clientInfo: this.getUniversalClientInfo(),
passwordSecret: this.config.passwordSecret
})
const {
passwordHash,
version
} = passwordUtils.generatePasswordHash({
password
})
const data = {
username,
password: passwordHash,
password_secret_version: version,
dcloud_appid: authorizedApp || [],
nickname,
role: role || [],
mobile,
email,
tags: tags || [],
status
}
if (email) {
data.email_confirmed = 1
}
if (mobile) {
data.mobile_confirmed = 1
}
// 触发 beforeRegister 钩子
const beforeRegister = this.hooks.beforeRegister
let userRecord = data
if (beforeRegister) {
userRecord = await beforeRegister({
userRecord,
clientInfo: this.getUniversalClientInfo()
})
}
await userCollection.add(userRecord)
return {
errCode: 0,
errMsg: ''
}
}

View File

@ -0,0 +1,4 @@
module.exports = {
addUser: require('./add-user'),
updateUser: require('./update-user')
}

View File

@ -0,0 +1,138 @@
const {
findUser
} = require('../../lib/utils/account')
const {
ERROR
} = require('../../common/error')
const {
userCollection
} = require('../../common/constants')
const PasswordUtils = require('../../lib/utils/password')
/**
* 修改用户
* @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#update-user
* @param {Object} params
* @param {String} params.uid 要更新的用户id
* @param {String} params.username 用户名
* @param {String} params.password 密码
* @param {String} params.nickname 昵称
* @param {Array} params.authorizedApp 允许登录的AppID列表
* @param {Array} params.role 用户角色列表
* @param {String} params.mobile 手机号
* @param {String} params.email 邮箱
* @param {Array} params.tags 用户标签
* @param {Number} params.status 用户状态
* @returns
*/
module.exports = async function (params = {}) {
const schema = {
uid: 'string',
username: 'username',
password: {
required: false,
type: 'password'
},
authorizedApp: {
required: false,
type: 'array<string>'
}, // 指定允许登录的app传空数组或不传时表示可以不可以在任何端登录
nickname: {
required: false,
type: 'nickname'
},
role: {
require: false,
type: 'array<string>'
},
mobile: {
required: false,
type: 'mobile'
},
email: {
required: false,
type: 'email'
},
tags: {
required: false,
type: 'array<string>'
},
status: {
required: false,
type: 'number'
}
}
this.middleware.validate(params, schema)
const {
uid,
username,
password,
authorizedApp,
nickname,
role,
mobile,
email,
tags,
status
} = params
// 更新的用户数据字段
const data = {
username,
dcloud_appid: authorizedApp,
nickname,
role,
mobile,
email,
tags,
status
}
const realData = Object.keys(data).reduce((res, key) => {
const item = data[key]
if (item !== undefined) {
res[key] = item
}
return res
}, {})
// 更新用户名时验证用户名是否重新
if (username) {
const {
userMatched
} = await findUser({
userQuery: {
username
},
authorizedApp
})
if (userMatched.filter(user => user._id !== uid).length) {
throw {
errCode: ERROR.ACCOUNT_EXISTS
}
}
}
if (password) {
const passwordUtils = new PasswordUtils({
clientInfo: this.getUniversalClientInfo(),
passwordSecret: this.config.passwordSecret
})
const {
passwordHash,
version
} = passwordUtils.generatePasswordHash({
password
})
realData.password = passwordHash
realData.password_secret_version = version
}
await userCollection.doc(uid).update(realData)
return {
errCode: 0
}
}

View File

@ -0,0 +1,70 @@
function isMobileCodeSupported () {
const config = this.config
return !!(config.service && config.service.sms && config.service.sms.smsKey)
}
function isUniverifySupport () {
return true
}
function isWeixinSupported () {
this.configUtils.getOauthConfig({
provider: 'weixin'
})
return true
}
function isQQSupported () {
this.configUtils.getOauthConfig({
provider: 'qq'
})
return true
}
function isAppleSupported () {
this.configUtils.getOauthConfig({
provider: 'apple'
})
return true
}
function isAlipaySupported () {
this.configUtils.getOauthConfig({
provider: 'alipay'
})
return true
}
const loginTypeTester = {
'mobile-code': isMobileCodeSupported,
univerify: isUniverifySupport,
weixin: isWeixinSupported,
qq: isQQSupported,
apple: isAppleSupported,
alipay: isAlipaySupported
}
/**
* 获取支持的登录方式
* @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#get-supported-login-type
* @returns
*/
module.exports = async function () {
const supportedLoginType = [
'username-password',
'mobile-password',
'email-password'
]
for (const type in loginTypeTester) {
try {
if (loginTypeTester[type].call(this)) {
supportedLoginType.push(type)
}
} catch (error) { }
}
return {
errCode: 0,
errMsg: '',
supportedLoginType
}
}

View File

@ -0,0 +1,3 @@
module.exports = {
getSupportedLoginType: require('./get-supported-login-type')
}

View File

@ -0,0 +1,5 @@
module.exports = {
externalRegister: require('./register'),
externalLogin: require('./login'),
updateUserInfoByExternal: require('./update-user-info')
}

View File

@ -0,0 +1,68 @@
const { preLogin, postLogin } = require('../../lib/utils/login')
const { EXTERNAL_DIRECT_CONNECT_PROVIDER } = require('../../common/constants')
const { ERROR } = require('../../common/error')
/**
* 外部用户登录
* @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#external-login
* @param {object} params
* @param {string} params.uid uni-id体系用户id
* @param {string} params.externalUid 业务系统的用户id
* @returns {object}
*/
module.exports = async function (params = {}) {
const schema = {
uid: {
required: false,
type: 'string'
},
externalUid: {
required: false,
type: 'string'
}
}
this.middleware.validate(params, schema)
const {
uid,
externalUid
} = params
if (!uid && !externalUid) {
throw {
errCode: ERROR.PARAM_REQUIRED,
errMsgValue: {
param: 'uid or externalUid'
}
}
}
let query
if (uid) {
query = {
_id: uid
}
} else {
query = {
identities: {
provider: EXTERNAL_DIRECT_CONNECT_PROVIDER,
uid: externalUid
}
}
}
const user = await preLogin.call(this, {
user: query
})
const result = await postLogin.call(this, {
user
})
return {
errCode: result.errCode,
newToken: result.newToken,
uid: result.uid
}
}

View File

@ -0,0 +1,93 @@
const url = require('url')
const { preRegister, postRegister } = require('../../lib/utils/register')
const { EXTERNAL_DIRECT_CONNECT_PROVIDER } = require('../../common/constants')
/**
* 外部注册用户
* @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#external-register
* @param {object} params
* @param {string} params.externalUid 业务系统的用户id
* @param {string} params.nickname 昵称
* @param {number} params.gender 性别
* @param {string} params.avatar 头像
* @returns {object}
*/
module.exports = async function (params = {}) {
const schema = {
externalUid: 'string',
nickname: {
required: false,
type: 'nickname'
},
gender: {
required: false,
type: 'number'
},
avatar: {
required: false,
type: 'string'
}
}
this.middleware.validate(params, schema)
const {
externalUid,
avatar,
gender,
nickname
} = params
await preRegister.call(this, {
user: {
identities: {
provider: EXTERNAL_DIRECT_CONNECT_PROVIDER,
uid: externalUid
}
}
})
const extraData = {}
if (avatar) {
// eslint-disable-next-line n/no-deprecated-api
const avatarPath = url.parse(avatar).pathname
const extName = avatarPath.indexOf('.') > -1 ? avatarPath.split('.').pop() : ''
extraData.avatar_file = {
name: avatarPath,
extname: extName,
url: avatar
}
}
const result = await postRegister.call(this, {
user: {
avatar,
gender,
nickname,
identities: [
{
provider: EXTERNAL_DIRECT_CONNECT_PROVIDER,
userInfo: {
avatar,
gender,
nickname
},
uid: externalUid
}
]
},
extraData
})
return {
errCode: result.errCode,
newToken: result.newToken,
externalUid,
avatar,
gender,
nickname,
uid: result.uid
}
}

View File

@ -0,0 +1,208 @@
const url = require('url')
const { userCollection, EXTERNAL_DIRECT_CONNECT_PROVIDER } = require('../../common/constants')
const { ERROR } = require('../../common/error')
const { findUser } = require('../../lib/utils/account')
const PasswordUtils = require('../../lib/utils/password')
/**
* 使用 uid 或 externalUid 获取用户信息
* @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#external-update-userinfo
* @param {object} params
* @param {string} params.uid uni-id体系的用户id
* @param {string} params.externalUid 业务系统的用户id
* @param {string} params.nickname 昵称
* @param {string} params.gender 性别
* @param {string} params.avatar 头像
* @returns {object}
*/
module.exports = async function (params = {}) {
const schema = {
uid: {
required: false,
type: 'string'
},
externalUid: {
required: false,
type: 'string'
},
username: {
required: false,
type: 'string'
},
password: {
required: false,
type: 'password'
},
authorizedApp: {
required: false,
type: 'array<string>'
}, // 指定允许登录的app传空数组或不传时表示可以不可以在任何端登录
nickname: {
required: false,
type: 'nickname'
},
role: {
require: false,
type: 'array<string>'
},
mobile: {
required: false,
type: 'mobile'
},
email: {
required: false,
type: 'email'
},
tags: {
required: false,
type: 'array<string>'
},
status: {
required: false,
type: 'number'
},
gender: {
required: false,
type: 'number'
},
avatar: {
required: false,
type: 'string'
}
}
this.middleware.validate(params, schema)
const {
uid,
externalUid,
username,
password,
authorizedApp,
nickname,
role,
mobile,
email,
tags,
status,
avatar,
gender
} = params
if (!uid && !externalUid) {
throw {
errCode: ERROR.PARAM_REQUIRED,
errMsgValue: {
param: 'uid or externalUid'
}
}
}
let query
if (uid) {
query = {
_id: uid
}
} else {
query = {
identities: {
provider: EXTERNAL_DIRECT_CONNECT_PROVIDER,
uid: externalUid
}
}
}
const users = await userCollection.where(query).get()
const user = users.data && users.data[0]
if (!user) {
throw {
errCode: ERROR.ACCOUNT_NOT_EXISTS
}
}
// 更新的用户数据字段
const data = {
username,
dcloud_appid: authorizedApp,
nickname,
role,
mobile,
email,
tags,
status,
avatar,
gender
}
const realData = Object.keys(data).reduce((res, key) => {
const item = data[key]
if (item !== undefined) {
res[key] = item
}
return res
}, {})
// 更新用户名时验证用户名是否重新
if (username) {
const {
userMatched
} = await findUser({
userQuery: {
username
},
authorizedApp
})
if (userMatched.filter(user => user._id !== uid).length) {
throw {
errCode: ERROR.ACCOUNT_EXISTS
}
}
}
if (password) {
const passwordUtils = new PasswordUtils({
clientInfo: this.getUniversalClientInfo(),
passwordSecret: this.config.passwordSecret
})
const {
passwordHash,
version
} = passwordUtils.generatePasswordHash({
password
})
realData.password = passwordHash
realData.password_secret_version = version
}
if (avatar) {
// eslint-disable-next-line n/no-deprecated-api
const avatarPath = url.parse(avatar).pathname
const extName = avatarPath.indexOf('.') > -1 ? avatarPath.split('.').pop() : ''
realData.avatar_file = {
name: avatarPath,
extname: extName,
url: avatar
}
}
if (user.identities.length) {
const identity = user.identities.find(item => item.provider === EXTERNAL_DIRECT_CONNECT_PROVIDER)
if (identity) {
identity.userInfo = {
avatar,
gender,
nickname
}
}
realData.identities = user.identities
}
await userCollection.where(query).update(realData)
return {
errCode: 0
}
}

View File

@ -0,0 +1,136 @@
const { userCollection, REAL_NAME_STATUS, frvLogsCollection } = require('../../common/constants')
const { dataDesensitization, catchAwait } = require('../../common/utils')
const { encryptData, decryptData } = require('../../common/sensitive-aes-cipher')
const { ERROR } = require('../../common/error')
/**
* 查询认证结果
* @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#get-frv-auth-result
* @param {Object} params
* @param {String} params.certifyId 认证ID
* @returns
*/
module.exports = async function (params) {
const schema = {
certifyId: 'string'
}
this.middleware.validate(params, schema)
const { uid } = this.authInfo // 从authInfo中取出uid属性
const { certifyId } = params // 从params中取出certifyId属性
const user = await userCollection.doc(uid).get() // 根据uid查询用户信息
const userInfo = user.data && user.data[0] // 从查询结果中获取userInfo对象
// 如果用户不存在,抛出账户不存在的错误
if (!userInfo) {
throw {
errCode: ERROR.ACCOUNT_NOT_EXISTS
}
}
const { realname_auth: realNameAuth = {} } = userInfo
// 如果用户已经实名认证,抛出已实名认证的错误
if (realNameAuth.auth_status === REAL_NAME_STATUS.CERTIFIED) {
throw {
errCode: ERROR.REAL_NAME_VERIFIED
}
}
// 初始化实人认证服务
const frvManager = uniCloud.getFacialRecognitionVerifyManager({
requestId: this.getUniCloudRequestId()
})
// 调用frvManager的getAuthResult方法获取认证结果
const [error, res] = await catchAwait(frvManager.getAuthResult({
certifyId
}))
// 如果出现错误,抛出未知错误并打印日志
if (error) {
console.log(ERROR.UNKNOWN_ERROR, 'error: ', error)
throw error
}
// 如果认证状态为“PROCESSING”抛出认证正在处理中的错误
if (res.authState === 'PROCESSING') {
throw {
errCode: ERROR.FRV_PROCESSING
}
}
// 如果认证状态为“FAIL”更新认证日志的状态并抛出认证失败的错误
if (res.authState === 'FAIL') {
await frvLogsCollection.where({
certify_id: certifyId
}).update({
status: REAL_NAME_STATUS.CERTIFY_FAILED
})
console.log(ERROR.FRV_FAIL, 'error: ', res)
throw {
errCode: ERROR.FRV_FAIL
}
}
// 如果认证状态不为“SUCCESS”抛出未知错误并打印日志
if (res.authState !== 'SUCCESS') {
console.log(ERROR.UNKNOWN_ERROR, 'source res: ', res)
throw {
errCode: ERROR.UNKNOWN_ERROR
}
}
// 根据certifyId查询认证记录
const frvLogs = await frvLogsCollection.where({
certify_id: certifyId
}).get()
const log = frvLogs.data && frvLogs.data[0]
const updateData = {
realname_auth: {
auth_status: REAL_NAME_STATUS.CERTIFIED,
real_name: log.real_name,
identity: log.identity,
auth_date: Date.now(),
type: 0
}
}
// 如果获取到了认证照片的地址则会对其进行下载并使用uniCloud.uploadFile方法将其上传到云存储并将上传后的fileID保存起来。
if (res.pictureUrl) {
const pictureRes = await uniCloud.httpclient.request(res.pictureUrl)
if (pictureRes.status < 400) {
const {
fileID
} = await uniCloud.uploadFile({
cloudPath: `user/id-card/${uid}.b64`,
cloudPathAsRealPath: true,
fileContent: Buffer.from(encryptData.call(this, pictureRes.data.toString('base64')))
})
updateData.realname_auth.in_hand = fileID
}
}
await Promise.all([
// 更新用户认证状态
userCollection.doc(uid).update(updateData),
// 更新实人认证记录状态
frvLogsCollection.where({
certify_id: certifyId
}).update({
status: REAL_NAME_STATUS.CERTIFIED
})
])
return {
errCode: 0,
authStatus: REAL_NAME_STATUS.CERTIFIED,
realName: dataDesensitization(decryptData.call(this, log.real_name), { onlyLast: true }), // 对姓名进行脱敏处理
identity: dataDesensitization(decryptData.call(this, log.identity)) // 对身份证号进行脱敏处理
}
}

View File

@ -0,0 +1,99 @@
const { userCollection, REAL_NAME_STATUS, frvLogsCollection, dbCmd } = require('../../common/constants')
const { ERROR } = require('../../common/error')
const { encryptData } = require('../../common/sensitive-aes-cipher')
const { getCurrentDateTimestamp } = require('../../common/utils')
// const CertifyIdExpired = 25 * 60 * 1000 // certifyId 过期时间为30分钟在25分时置为过期
/**
* 获取认证ID
* @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#get-frv-certify-id
* @param {Object} params
* @param {String} params.realName 真实姓名
* @param {String} params.idCard 身份证号码
* @param {String} params.metaInfo 客户端初始化时返回的metaInfo
* @returns
*/
module.exports = async function (params) {
const schema = {
realName: 'realName',
idCard: 'idCard',
metaInfo: 'string'
}
this.middleware.validate(params, schema)
const { realName: originalRealName, idCard: originalIdCard, metaInfo } = params // 解构出传入参数的真实姓名、身份证号码、其他元数据
const realName = encryptData.call(this, originalRealName) // 对真实姓名进行加密处理
const idCard = encryptData.call(this, originalIdCard) // 对身份证号码进行加密处理
const { uid } = this.authInfo // 获取当前用户的 ID
const idCardCertifyLimit = this.config.idCardCertifyLimit || 1 // 获取身份证认证限制次数默认为1次
const realNameCertifyLimit = this.config.realNameCertifyLimit || 5 // 获取实名认证限制次数默认为5次
const frvNeedAlivePhoto = this.config.frvNeedAlivePhoto || false // 是否需要拍摄活体照片,默认为 false
const user = await userCollection.doc(uid).get() // 获取用户信息
const userInfo = user.data && user.data[0] // 获取用户信息对象中的实名认证信息
const { realname_auth: realNameAuth = {} } = userInfo // 解构出实名认证信息中的认证状态对象,默认为空对象
// 如果用户已经实名认证过,不能再次认证
if (realNameAuth.auth_status === REAL_NAME_STATUS.CERTIFIED) {
throw {
errCode: ERROR.REAL_NAME_VERIFIED
}
}
// 查询已经使用同一个身份证认证的账号数量,如果超过限制则不能认证
const idCardAccount = await userCollection.where({
realname_auth: {
type: 0, // 用户认证状态是个人
auth_status: REAL_NAME_STATUS.CERTIFIED, // 认证状态为已认证
identity: idCard // 身份证号码和传入参数的身份证号码相同
}
}).get()
if (idCardAccount.data.length >= idCardCertifyLimit) {
throw {
errCode: ERROR.ID_CARD_EXISTS
}
}
// 查询用户今天已经进行的实名认证次数,如果超过限制则不能认证
const userFrvLogs = await frvLogsCollection.where({
user_id: uid,
created_date: dbCmd.gt(getCurrentDateTimestamp()) // 查询今天的认证记录
}).get()
// 限制用户每日认证次数
if (userFrvLogs.data && userFrvLogs.data.length >= realNameCertifyLimit) {
throw {
errCode: ERROR.REAL_NAME_VERIFY_UPPER_LIMIT
}
}
// 初始化实人认证服务
const frvManager = uniCloud.getFacialRecognitionVerifyManager({
requestId: this.getUniCloudRequestId() // 获取当前
})
// 调用实人认证服务,获取认证 ID
const res = await frvManager.getCertifyId({
realName: originalRealName,
idCard: originalIdCard,
needPicture: frvNeedAlivePhoto,
metaInfo
})
// 将认证记录插入到实名认证日志中
await frvLogsCollection.add({
user_id: uid,
certify_id: res.certifyId,
real_name: realName,
identity: idCard,
status: REAL_NAME_STATUS.WAITING_CERTIFIED,
created_date: Date.now()
})
// 返回认证ID
return {
certifyId: res.certifyId
}
}

View File

@ -0,0 +1,4 @@
module.exports = {
getFrvCertifyId: require('./get-certify-id'),
getFrvAuthResult: require('./get-auth-result')
}

View File

@ -0,0 +1,25 @@
const {
acceptInvite
} = require('../../lib/utils/fission')
/**
* 接受邀请
* @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#accept-invite
* @param {Object} params
* @param {String} params.inviteCode 邀请码
* @returns
*/
module.exports = async function (params = {}) {
const schema = {
inviteCode: 'string'
}
this.middleware.validate(params, schema)
const {
inviteCode
} = params
const uid = this.authInfo.uid
return acceptInvite({
uid,
inviteCode
})
}

View File

@ -0,0 +1,80 @@
const {
userCollection
} = require('../../common/constants')
const {
coverMobile
} = require('../../common/utils')
/**
* 获取受邀用户
* @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#get-invited-user
* @param {Object} params
* @param {Number} params.level 获取受邀用户的级数1表示直接邀请的用户
* @param {Number} params.limit 返回数据大小
* @param {Number} params.offset 返回数据偏移
* @param {Boolean} params.needTotal 是否需要返回总数
* @returns
*/
module.exports = async function (params = {}) {
const schema = {
level: 'number',
limit: {
required: false,
type: 'number'
},
offset: {
required: false,
type: 'number'
},
needTotal: {
required: false,
type: 'boolean'
}
}
this.middleware.validate(params, schema)
const {
level,
limit = 20,
offset = 0,
needTotal = false
} = params
const uid = this.authInfo.uid
const query = {
[`inviter_uid.${level - 1}`]: uid
}
const getUserRes = await userCollection.where(query)
.field({
_id: true,
avatar: true,
avatar_file: true,
username: true,
nickname: true,
mobile: true,
invite_time: true
})
.orderBy('invite_time', 'desc')
.skip(offset)
.limit(limit)
.get()
const invitedUser = getUserRes.data.map(item => {
return {
uid: item._id,
username: item.username,
nickname: item.nickname,
mobile: coverMobile(item.mobile),
inviteTime: item.invite_time,
avatar: item.avatar,
avatarFile: item.avatar_file
}
})
const result = {
errCode: 0,
invitedUser
}
if (needTotal) {
const getTotalRes = await userCollection.where(query).count()
result.total = getTotalRes.total
}
return result
}

View File

@ -0,0 +1,4 @@
module.exports = {
acceptInvite: require('./accept-invite'),
getInvitedUser: require('./get-invited-user')
}

View File

@ -0,0 +1,20 @@
module.exports = {
login: require('./login'),
loginBySms: require('./login-by-sms'),
loginByUniverify: require('./login-by-univerify'),
loginByWeixin: require('./login-by-weixin'),
loginByAlipay: require('./login-by-alipay'),
loginByQQ: require('./login-by-qq'),
loginByApple: require('./login-by-apple'),
loginByBaidu: require('./login-by-baidu'),
loginByDingtalk: require('./login-by-dingtalk'),
loginByToutiao: require('./login-by-toutiao'),
loginByDouyin: require('./login-by-douyin'),
loginByWeibo: require('./login-by-weibo'),
loginByTaobao: require('./login-by-taobao'),
loginByEmailLink: require('./login-by-email-link'),
loginByEmailCode: require('./login-by-email-code'),
loginByFacebook: require('./login-by-facebook'),
loginByGoogle: require('./login-by-google'),
loginByWeixinMobile: require('./login-by-weixin-mobile')
}

View File

@ -0,0 +1,70 @@
const {
initAlipay
} = require('../../lib/third-party/index')
const {
ERROR
} = require('../../common/error')
const {
preUnifiedLogin,
postUnifiedLogin
} = require('../../lib/utils/unified-login')
const {
LOG_TYPE
} = require('../../common/constants')
/**
* 支付宝登录
* @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#login-by-alipay
* @param {Object} params
* @param {String} params.code 支付宝小程序客户端登录返回的code
* @param {String} params.inviteCode 邀请码
* @returns
*/
module.exports = async function (params = {}) {
const schema = {
code: 'string',
inviteCode: {
type: 'string',
required: false
}
}
this.middleware.validate(params, schema)
const {
code,
inviteCode
} = params
const alipayApi = initAlipay.call(this)
let getAlipayAccountResult
try {
getAlipayAccountResult = await alipayApi.code2Session(code)
} catch (error) {
console.error(error)
await this.middleware.uniIdLog({
success: false,
type: LOG_TYPE.LOGIN
})
throw {
errCode: ERROR.GET_THIRD_PARTY_ACCOUNT_FAILED
}
}
const {
openid
} = getAlipayAccountResult
const {
type,
user
} = await preUnifiedLogin.call(this, {
user: {
ali_openid: openid
}
})
return postUnifiedLogin.call(this, {
user,
extraData: {},
isThirdParty: true,
type,
inviteCode
})
}

View File

@ -0,0 +1,77 @@
const {
initApple
} = require('../../lib/third-party/index')
const {
ERROR
} = require('../../common/error')
const {
preUnifiedLogin,
postUnifiedLogin
} = require('../../lib/utils/unified-login')
const {
LOG_TYPE
} = require('../../common/constants')
/**
* 苹果登录
* @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#login-by-apple
* @param {Object} params
* @param {String} params.identityToken 苹果登录返回的identityToken
* @param {String} params.nickname 用户昵称
* @param {String} params.inviteCode 邀请码
* @returns
*/
module.exports = async function (params = {}) {
const schema = {
identityToken: 'string',
nickname: {
required: false,
type: 'nickname'
},
inviteCode: {
required: false,
type: 'string'
}
}
this.middleware.validate(params, schema)
const {
identityToken,
nickname,
inviteCode
} = params
const appleApi = initApple.call(this)
let verifyResult
try {
verifyResult = await appleApi.verifyIdentityToken(identityToken)
} catch (error) {
console.error(error)
await this.middleware.uniIdLog({
success: false,
type: LOG_TYPE.LOGIN
})
throw {
errCode: ERROR.GET_THIRD_PARTY_ACCOUNT_FAILED
}
}
const {
openid
} = verifyResult
const {
type,
user
} = await preUnifiedLogin.call(this, {
user: {
apple_openid: openid
}
})
return postUnifiedLogin.call(this, {
user,
extraData: {
nickname
},
isThirdParty: true,
type,
inviteCode
})
}

View File

@ -0,0 +1,9 @@
/**
* 百度登录
* @param {Object} params
* @returns
*/
module.exports = async function (params = {}) {
// 此接口暂未实现欢迎向我们提交pr
throw new Error('api[loginByBaidu] is not yet implemented')
}

View File

@ -0,0 +1,9 @@
/**
* 钉钉登录
* @param {Object} params
* @returns
*/
module.exports = async function (params = {}) {
// 此接口暂未实现欢迎向我们提交pr
throw new Error('api[loginByDingtalk] is not yet implemented')
}

View File

@ -0,0 +1,9 @@
/**
* 抖音登录
* @param {Object} params
* @returns
*/
module.exports = async function (params = {}) {
// 此接口暂未实现欢迎向我们提交pr
throw new Error('api[loginByDouyin] is not yet implemented')
}

View File

@ -0,0 +1,9 @@
/**
* 邮箱验证码登录
* @param {Object} params
* @returns
*/
module.exports = async function (params = {}) {
// 此接口暂未实现欢迎向我们提交pr
throw new Error('api[loginByEmailCode] is not yet implemented')
}

View File

@ -0,0 +1,9 @@
/**
* 邮箱点击链接登录
* @param {Object} params
* @returns
*/
module.exports = async function (params = {}) {
// 此接口暂未实现欢迎向我们提交pr
throw new Error('api[loginByEmailLink] is not yet implemented')
}

View File

@ -0,0 +1,9 @@
/**
* Facebook登录
* @param {Object} params
* @returns
*/
module.exports = async function (params = {}) {
// 此接口暂未实现欢迎向我们提交pr
throw new Error('api[loginByFacebook] is not yet implemented')
}

View File

@ -0,0 +1,9 @@
/**
* Google登录
* @param {Object} params
* @returns
*/
module.exports = async function (params = {}) {
// 此接口暂未实现欢迎向我们提交pr
throw new Error('api[loginByGoogle] is not yet implemented')
}

View File

@ -0,0 +1,167 @@
const {
initQQ
} = require('../../lib/third-party/index')
const {
ERROR
} = require('../../common/error')
const {
preUnifiedLogin,
postUnifiedLogin
} = require('../../lib/utils/unified-login')
const {
LOG_TYPE
} = require('../../common/constants')
const {
getQQPlatform,
generateQQCache,
saveQQUserKey
} = require('../../lib/utils/qq')
const url = require('url')
/**
* QQ登录
* @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#login-by-qq
* @param {Object} params
* @param {String} params.code QQ小程序登录返回的code参数
* @param {String} params.accessToken App端QQ登录返回的accessToken参数
* @param {String} params.accessTokenExpired accessToken过期时间由App端QQ登录返回的expires_in参数计算而来单位毫秒
* @param {String} params.inviteCode 邀请码
* @returns
*/
module.exports = async function (params = {}) {
const schema = {
code: {
type: 'string',
required: false
},
accessToken: {
type: 'string',
required: false
},
accessTokenExpired: {
type: 'number',
required: false
},
inviteCode: {
type: 'string',
required: false
}
}
this.middleware.validate(params, schema)
const {
code,
accessToken,
accessTokenExpired,
inviteCode
} = params
const {
appId
} = this.getUniversalClientInfo()
const qqApi = initQQ.call(this)
const qqPlatform = getQQPlatform.call(this)
let apiName
switch (qqPlatform) {
case 'mp':
apiName = 'code2Session'
break
case 'app':
apiName = 'getOpenidByToken'
break
default:
throw new Error('Unsupported qq platform')
}
let getQQAccountResult
try {
getQQAccountResult = await qqApi[apiName]({
code,
accessToken
})
} catch (error) {
console.error(error)
await this.middleware.uniIdLog({
success: false,
type: LOG_TYPE.LOGIN
})
throw {
errCode: ERROR.GET_THIRD_PARTY_ACCOUNT_FAILED
}
}
const {
openid,
unionid,
// 保存下面的字段
sessionKey // QQ小程序用户sessionKey
} = getQQAccountResult
const {
type,
user
} = await preUnifiedLogin.call(this, {
user: {
qq_openid: {
[qqPlatform]: openid
},
qq_unionid: unionid
}
})
const extraData = {
qq_openid: {
[`${qqPlatform}_${appId}`]: openid
},
qq_unionid: unionid
}
if (type === 'register' && qqPlatform !== 'mp') {
const {
nickname,
avatar
} = await qqApi.getUserInfo({
accessToken,
openid
})
if (avatar) {
// eslint-disable-next-line n/no-deprecated-api
const extName = url.parse(avatar).pathname.split('.').pop()
const cloudPath = `user/avatar/${openid.slice(-8) + Date.now()}-avatar.${extName}`
const getAvatarRes = await uniCloud.httpclient.request(avatar)
if (getAvatarRes.status >= 400) {
throw {
errCode: ERROR.GET_THIRD_PARTY_USER_INFO_FAILED
}
}
const {
fileID
} = await uniCloud.uploadFile({
cloudPath,
fileContent: getAvatarRes.data
})
extraData.avatar_file = {
name: cloudPath,
extname: extName,
url: fileID
}
}
extraData.nickname = nickname
}
await saveQQUserKey.call(this, {
openid,
sessionKey,
accessToken,
accessTokenExpired
})
return postUnifiedLogin.call(this, {
user,
extraData: {
...extraData,
...generateQQCache.call(this, {
openid,
sessionKey, // QQ小程序用户sessionKey
accessToken, // App端QQ用户accessToken
accessTokenExpired // App端QQ用户accessToken过期时间
})
},
isThirdParty: true,
type,
inviteCode
})
}

View File

@ -0,0 +1,99 @@
const {
getNeedCaptcha,
verifyCaptcha
} = require('../../lib/utils/captcha')
const {
verifyMobileCode
} = require('../../lib/utils/verify-code')
const {
preUnifiedLogin,
postUnifiedLogin
} = require('../../lib/utils/unified-login')
const {
CAPTCHA_SCENE,
SMS_SCENE,
LOG_TYPE
} = require('../../common/constants')
/**
* 短信验证码登录
* @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#login-by-sms
* @param {Object} params
* @param {String} params.mobile 手机号
* @param {String} params.code 短信验证码
* @param {String} params.captcha 图形验证码
* @param {String} params.inviteCode 邀请码
* @returns
*/
module.exports = async function (params = {}) {
const schema = {
mobile: 'mobile',
code: 'string',
captcha: {
required: false,
type: 'string'
},
inviteCode: {
required: false,
type: 'string'
}
}
this.middleware.validate(params, schema)
const {
mobile,
code,
captcha,
inviteCode
} = params
const needCaptcha = await getNeedCaptcha.call(this, {
mobile
})
if (needCaptcha) {
await verifyCaptcha.call(this, {
captcha,
scene: CAPTCHA_SCENE.LOGIN_BY_SMS
})
}
try {
await verifyMobileCode({
mobile,
code,
scene: SMS_SCENE.LOGIN_BY_SMS
})
} catch (error) {
console.log(error, {
mobile,
code,
type: SMS_SCENE.LOGIN_BY_SMS
})
await this.middleware.uniIdLog({
success: false,
data: {
mobile
},
type: LOG_TYPE.LOGIN
})
throw error
}
const {
type,
user
} = await preUnifiedLogin.call(this, {
user: {
mobile
}
})
return postUnifiedLogin.call(this, {
user,
extraData: {
mobile_confirmed: 1
},
isThirdParty: false,
type,
inviteCode
})
}

View File

@ -0,0 +1,9 @@
/**
* 淘宝登录
* @param {Object} params
* @returns
*/
module.exports = async function (params = {}) {
// 此接口暂未实现欢迎向我们提交pr
throw new Error('api[loginByTaobao] is not yet implemented')
}

View File

@ -0,0 +1,9 @@
/**
* 头条登录
* @param {Object} params
* @returns
*/
module.exports = async function (params = {}) {
// 此接口暂未实现欢迎向我们提交pr
throw new Error('api[loginByToutiao] is not yet implemented')
}

View File

@ -0,0 +1,69 @@
const {
getPhoneNumber
} = require('../../lib/utils/univerify')
const {
preUnifiedLogin,
postUnifiedLogin
} = require('../../lib/utils/unified-login')
const {
LOG_TYPE
} = require('../../common/constants')
/**
* App端一键登录
* @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#login-by-univerify
* @param {Object} params
* @param {String} params.access_token APP端一键登录返回的access_token
* @param {String} params.openid APP端一键登录返回的openid
* @param {String} params.inviteCode 邀请码
* @returns
*/
module.exports = async function (params = {}) {
const schema = {
access_token: 'string',
openid: 'string',
inviteCode: {
required: false,
type: 'string'
}
}
this.middleware.validate(params, schema)
const {
// eslint-disable-next-line camelcase
access_token,
openid,
inviteCode
} = params
let mobile
try {
const phoneInfo = await getPhoneNumber.call(this, {
// eslint-disable-next-line camelcase
access_token,
openid
})
mobile = phoneInfo.phoneNumber
} catch (error) {
await this.middleware.uniIdLog({
success: false,
type: LOG_TYPE.LOGIN
})
throw error
}
const {
user,
type
} = await preUnifiedLogin.call(this, {
user: {
mobile
}
})
return postUnifiedLogin.call(this, {
user,
extraData: {
mobile_confirmed: 1
},
type,
inviteCode
})
}

View File

@ -0,0 +1,9 @@
/**
* 微博登录
* @param {Object} params
* @returns
*/
module.exports = async function (params = {}) {
// 此接口暂未实现欢迎向我们提交pr
throw new Error('api[loginByWeibo] is not yet implemented')
}

View File

@ -0,0 +1,106 @@
const {
initWeixin
} = require('../../lib/third-party/index')
const {
getWeixinAccessToken
} = require('../../lib/utils/weixin')
const {
ERROR
} = require('../../common/error')
const {
preUnifiedLogin,
postUnifiedLogin
} = require('../../lib/utils/unified-login')
const {
LOG_TYPE
} = require('../../common/constants')
const {
preBind,
postBind
} = require('../../lib/utils/relate')
/**
* 微信授权手机号登录
* @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#login-by-weixin-mobile
* @param {Object} params
* @param {String} params.phoneCode 微信手机号返回的code
* @param {String} params.inviteCode 邀请码
* @returns
*/
module.exports = async function (params = {}) {
const schema = {
phoneCode: 'string',
inviteCode: {
type: 'string',
required: false
}
}
this.middleware.validate(params, schema)
const { phoneCode, inviteCode } = params
const weixinApi = initWeixin.call(this)
let mobile
try {
const accessToken = await getWeixinAccessToken.call(this)
const mobileRes = await weixinApi.getPhoneNumber(accessToken, phoneCode)
mobile = mobileRes.purePhoneNumber
} catch (error) {
console.error(error)
await this.middleware.uniIdLog({
success: false,
type: LOG_TYPE.LOGIN
})
throw {
errCode: ERROR.GET_THIRD_PARTY_ACCOUNT_FAILED
}
}
const { type, user } = await preUnifiedLogin.call(this, {
user: {
mobile
}
})
let extraData = {
mobile_confirmed: 1
}
if (type === 'login') {
// 绑定手机号
if (!user.mobile_confirmed) {
const bindAccount = {
mobile
}
await preBind.call(this, {
uid: user._id,
bindAccount,
logType: LOG_TYPE.BIND_MOBILE
})
await postBind.call(this, {
uid: user._id,
bindAccount,
extraData: {
mobile_confirmed: 1
},
logType: LOG_TYPE.BIND_MOBILE
})
extraData = {
...extraData,
...bindAccount
}
}
}
return postUnifiedLogin.call(this, {
user,
extraData: {
...extraData
},
isThirdParty: false,
type,
inviteCode
})
}

View File

@ -0,0 +1,176 @@
const {
initWeixin
} = require('../../lib/third-party/index')
const {
ERROR
} = require('../../common/error')
const {
preUnifiedLogin,
postUnifiedLogin
} = require('../../lib/utils/unified-login')
const {
generateWeixinCache,
getWeixinPlatform,
saveWeixinUserKey,
saveSecureNetworkCache
} = require('../../lib/utils/weixin')
const {
LOG_TYPE
} = require('../../common/constants')
const url = require('url')
/**
* 微信登录
* @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#login-by-weixin
* @param {Object} params
* @param {String} params.code 微信登录返回的code
* @param {String} params.inviteCode 邀请码
* @returns
*/
module.exports = async function (params = {}) {
const schema = {
code: 'string',
inviteCode: {
type: 'string',
required: false
}
}
this.middleware.validate(params, schema)
const {
code,
inviteCode,
// 内部参数,暂不暴露
secureNetworkCache = false
} = params
const {
appId
} = this.getUniversalClientInfo()
const weixinApi = initWeixin.call(this)
const weixinPlatform = getWeixinPlatform.call(this)
let apiName
switch (weixinPlatform) {
case 'mp':
apiName = 'code2Session'
break
case 'app':
case 'h5':
case 'web':
apiName = 'getOauthAccessToken'
break
default:
throw new Error('Unsupported weixin platform')
}
let getWeixinAccountResult
try {
getWeixinAccountResult = await weixinApi[apiName](code)
} catch (error) {
console.error(error)
await this.middleware.uniIdLog({
success: false,
type: LOG_TYPE.LOGIN
})
throw {
errCode: ERROR.GET_THIRD_PARTY_ACCOUNT_FAILED
}
}
const {
openid,
unionid,
// 保存下面四个字段
sessionKey, // 微信小程序用户sessionKey
accessToken, // App端微信用户accessToken
refreshToken, // App端微信用户refreshToken
expired: accessTokenExpired // App端微信用户accessToken过期时间
} = getWeixinAccountResult
if (secureNetworkCache) {
if (weixinPlatform !== 'mp') {
throw new Error('Unsupported weixin platform, expect mp-weixin')
}
await saveSecureNetworkCache.call(this, {
code,
openid,
unionid,
sessionKey
})
}
const {
type,
user
} = await preUnifiedLogin.call(this, {
user: {
wx_openid: {
[weixinPlatform]: openid
},
wx_unionid: unionid
}
})
const extraData = {
wx_openid: {
[`${weixinPlatform}_${appId}`]: openid
},
wx_unionid: unionid
}
if (type === 'register' && weixinPlatform !== 'mp') {
const {
nickname,
avatar
} = await weixinApi.getUserInfo({
accessToken,
openid
})
if (avatar) {
// eslint-disable-next-line n/no-deprecated-api
const avatarPath = url.parse(avatar).pathname
const extName = avatarPath.indexOf('.') > -1 ? url.parse(avatar).pathname.split('.').pop() : 'jpg'
const cloudPath = `user/avatar/${openid.slice(-8) + Date.now()}-avatar.${extName}`
const getAvatarRes = await uniCloud.httpclient.request(avatar)
if (getAvatarRes.status >= 400) {
throw {
errCode: ERROR.GET_THIRD_PARTY_USER_INFO_FAILED
}
}
const {
fileID
} = await uniCloud.uploadFile({
cloudPath,
fileContent: getAvatarRes.data
})
extraData.avatar_file = {
name: cloudPath,
extname: extName,
url: fileID
}
}
extraData.nickname = nickname
}
await saveWeixinUserKey.call(this, {
openid,
sessionKey,
accessToken,
refreshToken,
accessTokenExpired
})
return postUnifiedLogin.call(this, {
user,
extraData: {
...extraData,
...generateWeixinCache.call(this, {
openid,
sessionKey, // 微信小程序用户sessionKey
accessToken, // App端微信用户accessToken
refreshToken, // App端微信用户refreshToken
accessTokenExpired // App端微信用户accessToken过期时间
})
},
isThirdParty: true,
type,
inviteCode
})
}

View File

@ -0,0 +1,94 @@
const {
preLoginWithPassword,
postLogin
} = require('../../lib/utils/login')
const {
getNeedCaptcha,
verifyCaptcha
} = require('../../lib/utils/captcha')
const {
CAPTCHA_SCENE
} = require('../../common/constants')
const {
ERROR
} = require('../../common/error')
/**
* 用户名密码登录
* @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#login
* @param {Object} params
* @param {String} params.username 用户名
* @param {String} params.mobile 手机号
* @param {String} params.email 邮箱
* @param {String} params.password 密码
* @param {String} params.captcha 图形验证码
* @returns
*/
module.exports = async function (params = {}) {
const schema = {
username: {
required: false,
type: 'username'
},
mobile: {
required: false,
type: 'mobile'
},
email: {
required: false,
type: 'email'
},
password: 'password',
captcha: {
required: false,
type: 'string'
}
}
this.middleware.validate(params, schema)
const {
username,
mobile,
email,
password,
captcha
} = params
if (!username && !mobile && !email) {
throw {
errCode: ERROR.INVALID_USERNAME
}
} else if (
(username && email) ||
(username && mobile) ||
(email && mobile)
) {
throw {
errCode: ERROR.INVALID_PARAM
}
}
const needCaptcha = await getNeedCaptcha.call(this, {
username,
mobile,
email
})
if (needCaptcha) {
await verifyCaptcha.call(this, {
captcha,
scene: CAPTCHA_SCENE.LOGIN_BY_PWD
})
}
const {
user,
extraData
} = await preLoginWithPassword.call(this, {
user: {
username,
mobile,
email
},
password
})
return postLogin.call(this, {
user,
extraData
})
}

View File

@ -0,0 +1,3 @@
module.exports = {
logout: require('./logout')
}

View File

@ -0,0 +1,15 @@
const {
logout
} = require('../../lib/utils/logout')
/**
* 用户退出登录
* @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#logout
* @returns
*/
module.exports = async function () {
await logout.call(this)
return {
errCode: 0
}
}

View File

@ -0,0 +1,37 @@
const {
isAuthorizeApproved
} = require('./utils')
const {
dbCmd,
userCollection
} = require('../../common/constants')
/**
* 授权用户登录应用
* @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#authorize-app-login
* @param {Object} params
* @param {String} params.uid 用户id
* @param {String} params.appId 授权的应用的AppId
* @returns
*/
module.exports = async function (params = {}) {
const schema = {
uid: 'string',
appId: 'string'
}
this.middleware.validate(params, schema)
const {
uid,
appId
} = params
await isAuthorizeApproved({
uid,
appIdList: [appId]
})
await userCollection.doc(uid).update({
dcloud_appid: dbCmd.push(appId)
})
return {
errCode: 0
}
}

View File

@ -0,0 +1,5 @@
module.exports = {
authorizeAppLogin: require('./authorize-app-login'),
removeAuthorizedApp: require('./remove-authorized-app'),
setAuthorizedApp: require('./set-authorized-app')
}

View File

@ -0,0 +1,30 @@
const {
dbCmd,
userCollection
} = require('../../common/constants')
/**
* 移除用户登录授权
* @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#remove-authorized-app
* @param {Object} params
* @param {String} params.uid 用户id
* @param {String} params.appId 取消授权的应用的AppId
* @returns
*/
module.exports = async function (params = {}) {
const schema = {
uid: 'string',
appId: 'string'
}
this.middleware.validate(params, schema)
const {
uid,
appId
} = params
await userCollection.doc(uid).update({
dcloud_appid: dbCmd.pull(appId)
})
return {
errCode: 0
}
}

View File

@ -0,0 +1,36 @@
const {
isAuthorizeApproved
} = require('./utils')
const {
userCollection
} = require('../../common/constants')
/**
* 设置用户允许登录的应用列表
* @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#set-authorized-app
* @param {Object} params
* @param {String} params.uid 用户id
* @param {Array} params.appIdList 允许登录的应用AppId列表
* @returns
*/
module.exports = async function (params = {}) {
const schema = {
uid: 'string',
appIdList: 'array<string>'
}
this.middleware.validate(params, schema)
const {
uid,
appIdList
} = params
await isAuthorizeApproved({
uid,
appIdList
})
await userCollection.doc(uid).update({
dcloud_appid: appIdList
})
return {
errCode: 0
}
}

View File

@ -0,0 +1,38 @@
const {
userCollection
} = require('../../common/constants')
const {
ERROR
} = require('../../common/error')
const {
findUser
} = require('../../lib/utils/account')
async function isAuthorizeApproved ({
uid,
appIdList
} = {}) {
const getUserRes = await userCollection.doc(uid).get()
const userRecord = getUserRes.data[0]
if (!userRecord) {
throw {
errCode: ERROR.ACCOUNT_NOT_EXISTS
}
}
const {
userMatched
} = await findUser({
userQuery: userRecord,
authorizedApp: appIdList
})
if (userMatched.some(item => item._id !== uid)) {
throw {
errCode: ERROR.ACCOUNT_CONFLICT
}
}
}
module.exports = {
isAuthorizeApproved
}

View File

@ -0,0 +1,5 @@
module.exports = {
registerUser: require('./register-user'),
registerAdmin: require('./register-admin'),
registerUserByEmail: require('./register-user-by-email')
}

Some files were not shown because too many files have changed in this diff Show More