Source: sdk-file/src/web/auth.js

const utils = require('core-module/utils')
const HError = require('core-module/HError')
const constants = require('core-module/constants')
let thirdPartyAuthRequest = require('./thirdPartyAuthRequest')

/**
 * 获取授权结果
 * @function
 * @name getRedirectResult
 * @since v2.1.0
 * @memberof BaaS.auth
 * @return {Promise<BaaS.RedirectLoginResult>}
 */
const createGetRedirectResultFn = BaaS => () => {
  const url = new URL(window.location.href)
  let authResult
  try {
    authResult = JSON.parse(url.searchParams.get(constants.THIRD_PARTY_AUTH_URL_PARAM.AUTH_RESULT))
  } catch (err) {
    utils.log(constants.LOG_LEVEL.ERROR, err)
  }
  url.searchParams.delete(constants.THIRD_PARTY_AUTH_URL_PARAM.AUTH_RESULT)
  if (!authResult) {
    return Promise.reject(new HError(614, 'third party auth result not found'))
  } else if (
    authResult.status === constants.THIRD_PARTY_AUTH_STATUS.SUCCESS
    && authResult.action === constants.THIRD_PARTY_AUTH_HANDLER.LOGIN
  ) {
    history.replaceState && history.replaceState(null, '', url.toString())
    return BaaS.auth.getCurrentUser().then(user => {
      return Object.assign({}, authResult, {user})
    })
  } else {
    history.replaceState && history.replaceState(null, '', url.toString())
    return Promise.resolve(authResult)
  }
}

// “第三方登录”请求
let loginWithThirdPartyRequest = (BaaS, {provider, token, create_user, update_userprofile, silent_login} = {}) => {
  const _url = silent_login ? BaaS._config.API.WEB.THIRD_PARTY_SILENT_LOGIN : BaaS._config.API.WEB.THIRD_PARTY_LOGIN
  const _data = {
    auth_token: token,
    create_user: !!create_user,
  }
  if (!silent_login) {
    _data.update_userprofile = utils.getUpdateUserProfileParam(update_userprofile)
  }

  return BaaS.request({
    url: utils.format(_url, {provider}),
    method: 'POST',
    data: _data,
  }).then(utils.validateStatusCode).then(res => {
    // hack: silent_login 返回的数据结构不一致
    if (silent_login && res.data.user_info) {
      res.data.user_id = res.data.user_info.id
    }

    BaaS._polyfill.handleLoginSuccess(res)
  })
}

// “关联第三方账号”请求
let linkThirdPartyRequest = (BaaS, {provider, token, update_userprofile} = {}) => {
  return BaaS.request({
    url: utils.format(BaaS._config.API.WEB.THIRD_PARTY_ASSOCIATE, {provider}),
    method: 'POST',
    data: {
      auth_token: token,
      update_userprofile: utils.getUpdateUserProfileParam(update_userprofile),
    }
  })
}

// 回传信息至调用页面
let sendMessage = (mode, referer, authResult) => {
  if (mode === constants.THIRD_PARTY_AUTH_MODE.REDIRECT) {
    const refererUrl = new URL(referer)
    refererUrl.searchParams.set(constants.THIRD_PARTY_AUTH_URL_PARAM.AUTH_RESULT, JSON.stringify(authResult))
    window.location.href = refererUrl.toString()
  } else {
    const refererWindow = mode === constants.THIRD_PARTY_AUTH_MODE.POPUP_IFRAME
      ? window.parent
      : window.opener
    refererWindow.postMessage(authResult, referer)
  }
}

// 第三方授权成功后的操作。'login' 为登录,'associate' 为关联账号
const getHandler = handler => {
  const handlerList = [
    constants.THIRD_PARTY_AUTH_HANDLER.LOGIN,
    constants.THIRD_PARTY_AUTH_HANDLER.ASSOCIATE,
  ]
  if (handlerList.indexOf(handler) === -1) {
    throw new HError(614, `handler "${handler}" not found`)
  }
  return handler
}

const getErrorMsg = err => {
  let error = ''
  if (!err) return ''
  if (err.data && typeof err.data === 'object') {
    error = err.data.error_msg || err.data.message || err.data.error_message
  } else if (typeof err.data !== 'undefined') {
    error = err.data || err.statusText
  } else if (err.message) {   // error object
    error = err.message
  }
  return error
}

/*
 * 微信在 web 端 iframe 中授权时,在页面 URL 中添加 self_redirect 参数,使重定向发生在 iframe 中,
 * 参考 https://open.weixin.qq.com/cgi-bin/showdocument?action=dir_list&t=resource/res_list&verify=1&id=open1419316505&token=&lang=zh_CN
 */
const setExtraUrlParams = (url, options = {}) => {
  if (options.provider !== constants.THIRD_PARTY_AUTH_PROVIDER.WECHAT_WEB
    || options.mode !== constants.THIRD_PARTY_AUTH_MODE.POPUP_IFRAME) {
    return url
  }
  url = new URL(url)
  url.searchParams.set('self_redirect', true)
  if (!options.wechatIframeContentStyle) return url.toString()
  if (options.wechatIframeContentStyle.style) {
    url.searchParams.set('style', options.wechatIframeContentStyle.style)
  }
  if (options.wechatIframeContentStyle.href) {
    url.searchParams.set('href', options.wechatIframeContentStyle.href)
  }
  return url.toString()
}

/**
 * 跳转到第三方授权页面;获取 token 后调用 login 或 associate
 * @function
 * @name thirdPartyAuth
 * @since v2.1.0
 * @memberof BaaS.auth
 */
const createThirdPartyAuthFn = BaaS => () => {
  const PARAM = constants.THIRD_PARTY_AUTH_URL_PARAM
  const url = new URL(window.location.href)
  const params = url.searchParams
  const accessToken = params.get(PARAM.TOKEN)
  const provider = params.get(PARAM.PROVIDER)
  const referer = params.get(PARAM.REFERER)
  const mode = params.get(PARAM.MODE)
  const debug = params.get(PARAM.DEBUG)
  const handler = getHandler(params.get(PARAM.HANDLER))
  const create_user = params.get(PARAM.CREATE_USER)
  const update_userprofile = params.get(PARAM.UPDATE_USER_PROFILE)
  const silent_login = params.get(PARAM.SILENT_LOGIN)
  let wechatIframeContentStyle = {}
  try {
    wechatIframeContentStyle = JSON.parse(params.get(PARAM.WECHAT_IFRAME_CONTENT_STYLE))
  } catch (err) {
    utils.log(constants.LOG_LEVEL.ERROR, err)
  }
  const request = handler === constants.THIRD_PARTY_AUTH_HANDLER.LOGIN
    ? loginWithThirdPartyRequest
    : linkThirdPartyRequest
  if (accessToken) {
    // 授权成功
    return request(BaaS, {provider, token: accessToken, create_user, update_userprofile, silent_login})
      .then(() => {
        const authResult = {
          status: constants.THIRD_PARTY_AUTH_STATUS.SUCCESS,
          action: handler,
        }
        sendMessage(mode, referer, authResult)
      })
      .catch(err => {
        const error = getErrorMsg(err)
        const authResult = {
          status: constants.THIRD_PARTY_AUTH_STATUS.FAIL,
          error,
          action: handler,
        }
        if (mode !== constants.THIRD_PARTY_AUTH_MODE.REDIRECT || !debug) {
          sendMessage(mode, referer, authResult)
        }
      })
  } else {
    const _data = {
      callback_url: window.location.href,
    }
    if (silent_login) {
      _data.silent_login = true
    }

    // 跳转到第三方授权页面
    return BaaS.request({
      url: utils.format(BaaS._config.API.WEB.THIRD_PARTY_AUTH, {provider}),
      method: 'POST',
      data: _data,
    }).then(res => {
      if (res.status === constants.STATUS_CODE.SUCCESS && res.data.status === 'ok') {
        const url = setExtraUrlParams(res.data.redirect_url, {provider, mode, wechatIframeContentStyle})
        window.location.href = url
      } else {
        throw res
      }
    }).catch(err => {
      const error = getErrorMsg(err)
      const authResult = {
        status: constants.THIRD_PARTY_AUTH_STATUS.FAIL,
        error,
        action: handler,
      }
      if (mode !== constants.THIRD_PARTY_AUTH_MODE.REDIRECT || !debug) {
        sendMessage(mode, referer, authResult)
      }
      utils.log(constants.LOG_LEVEL.ERROR, err)
    })
  }
}

/**
 * 第三方登录<br />
 * <br />
 * 弹窗模式时序图:<br />
 * <img src="../images/login-with-third-party.png"><br />
 * <br />
 * 跳转模式时序图:<br />
 * <img src="../images/login-with-third-party-redirect.png"><br />
 *
 * @startuml login-with-third-party-popup.png
 *   用户 -> 发起登录的页面: 调用 loginWithThirdParty
 *   发起登录的页面 -> 授权页面: 打开
 *   授权页面 -> 授权页面: 调用 thirdPartyAuth
 *   授权页面 -> 第三方授权页面: 页面跳转
 *   第三方授权页面 -> 用户: 请求授权
 *   用户 -> 第三方授权页面: 授权
 *   第三方授权页面 -> 授权页面: 页面跳转(with token)
 *   授权页面 -> 授权页面: 调用 thirdPartyAuth
 *   授权页面 -> 后端: 请求 login 接口
 *   后端 -> 授权页面: 返回登录结果
 *   授权页面 -> 发起登录的页面: 返回登录结果
 *   发起登录的页面 -> 用户: 返回登录结果
 * @enduml
 *
 * @startuml login-with-third-party-redirect.png
 *   用户 -> 发起登录的页面: 调用 getRedirectResult
 *   发起登录的页面 -> 用户: 未找到登录结果
 *   用户 -> 发起登录的页面: 调用 loginWithThirdParty
 *   发起登录的页面 -> 授权页面: 跳转
 *   授权页面 -> 授权页面: 调用 thirdPartyAuth
 *   授权页面 -> 第三方授权页面: 页面跳转
 *   第三方授权页面 -> 用户: 请求授权
 *   用户 -> 第三方授权页面: 授权
 *   第三方授权页面 -> 授权页面: 页面跳转(with token)
 *   授权页面 -> 授权页面: 调用 thirdPartyAuth
 *   授权页面 -> 后端: 请求 login 接口
 *   后端 -> 授权页面: 返回登录结果
 *   授权页面 -> 发起登录的页面: 跳转回登录页面
 *   发起登录的页面 -> 用户: 返回登录结果
 *   用户 -> 发起登录的页面: 调用 getRedirectResult
 *   发起登录的页面 -> 用户: 返回登录结果
 * @enduml
 *
 * @function
 * @name loginWithThirdParty
 * @since v2.1.0
 * @memberof BaaS.auth
 * @param {string} provider 第三方平台
 * @param {string} authPageUrl 授权页面 URL
 * @param {BaaS.ThirdPartyLoginOptions} [options] 其他选项
 * @return {Promise<BaaS.CurrentUser>}
 */
const createLoginWithThirdPartyFn = BaaS => (provider, authPageUrl, options = {}) => {
  return thirdPartyAuthRequest(Object.assign({}, options, {
    provider,
    authPageUrl,
    handler: constants.THIRD_PARTY_AUTH_HANDLER.LOGIN,
  }))
    .then(() => BaaS.auth.getCurrentUser())
}

module.exports = function (BaaS) {
  BaaS.auth.silentLogin = utils.fnUnsupportedHandler
  BaaS.auth.thirdPartyAuth = utils.rateLimit(createThirdPartyAuthFn(BaaS))
  BaaS.auth.loginWithThirdParty = utils.rateLimit(createLoginWithThirdPartyFn(BaaS))
  BaaS.auth.getRedirectResult = createGetRedirectResultFn(BaaS)
}

module.exports._getHandler = getHandler
module.exports._getErrorMsg = getErrorMsg
module.exports._loginWithThirdPartyRequest = loginWithThirdPartyRequest
module.exports._linkThirdPartyRequest = linkThirdPartyRequest
module.exports._setExtraUrlParams = setExtraUrlParams
module.exports._sendMessage = sendMessage