|
|
@@ -8,19 +8,23 @@
|
|
|
* 所有接口返回统一格式的会话数据:{ userinfo, statistics, token, ... }
|
|
|
*/
|
|
|
|
|
|
-import { post } from '@/core/http/http-client'
|
|
|
+import { get, post } from '@/core/http/http-client'
|
|
|
import { buildRestPath } from '@/core/config/api-paths'
|
|
|
import { STORAGE } from '@/core/constants/storage-keys'
|
|
|
//import { googleLogin as pluginGoogleLogin } from '@/uni_modules/uni-oauth-google'
|
|
|
|
|
|
const ROUTE__LOGIN = buildRestPath('user/third')
|
|
|
+
|
|
|
+const ROUTE__LINE_AUTH_URL = '/line/getAuthUrl'//LINE 授权地址接口
|
|
|
+const ROUTE__LINE_GET_USERINFO = '/line/getuserinfo'//LINE 登录换取用户信息接口
|
|
|
+const FORM_URLENCODED_HEADER = { 'content-type': 'application/x-www-form-urlencoded' }//后端按表单读取 ticket,这里统一声明表单请求头
|
|
|
+const LINE_SUCCESS_PATHS = ['/api/line/loginsuccess', '/line/loginsuccess', '/line/login-success']//LINE 登录成功路径
|
|
|
+const LINE_FAIL_PATHS = ['/api/line/loginfail', '/line/loginfail', '/line/login-fail']//LINE 登录失败路径
|
|
|
// 谷歌登录配置web client_id 335082713048-el1qosjevi6a8elp80rrq77798msfkij.apps.googleusercontent.com
|
|
|
//const GOOGLE_SERVER_CLIENT_ID = '335082713048-el1qosjevi6a8elp80rrq77798msfkij.apps.googleusercontent.com'
|
|
|
/**
|
|
|
* LINE 登录配置
|
|
|
*/
|
|
|
-const LINE_CHANNEL_ID = '2010891313'
|
|
|
-const LINE_CALLBACK_URL = 'https://backend.amazewayhk.com/auth/line/callback'
|
|
|
function readClientHandle() {
|
|
|
return uni.getStorageSync(STORAGE.chatClient) || null
|
|
|
}
|
|
|
@@ -36,6 +40,97 @@ function unwrapSession(raw) {
|
|
|
return raw
|
|
|
}
|
|
|
|
|
|
+/**
|
|
|
+ * 从后端不同响应结构中提取 LINE 授权地址。
|
|
|
+ * 新流程先 GET /line/getAuthUrl,再把接口返回的授权地址放进 WebView。
|
|
|
+ */
|
|
|
+function pickLineAuthUrl(raw) {
|
|
|
+ if (!raw) return ''
|
|
|
+ if (typeof raw === 'string') return raw.trim()
|
|
|
+ const candidates = [
|
|
|
+ raw.url,
|
|
|
+ raw.auth_url,
|
|
|
+ raw.authUrl,
|
|
|
+ raw.authorize_url,
|
|
|
+ raw.authorizeUrl,
|
|
|
+ raw.redirect_url,
|
|
|
+ raw.redirectUrl,
|
|
|
+ raw.data && raw.data.url,
|
|
|
+ raw.data && raw.data.auth_url,
|
|
|
+ raw.data && raw.data.authUrl
|
|
|
+ ]
|
|
|
+ const found = candidates.find(item => typeof item === 'string' && item.trim())
|
|
|
+ return found ? found.trim() : ''
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * 安全读取 WebView 当前 URL。
|
|
|
+ * 真机上部分阶段 getURL 可能不可用,统一兜底为空字符串,避免调试监听中断登录流程。
|
|
|
+ */
|
|
|
+function readWebviewUrl(wv) {
|
|
|
+ try {
|
|
|
+ return wv && typeof wv.getURL === 'function' ? wv.getURL() : ''
|
|
|
+ } catch (e) {
|
|
|
+ return ''
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * 判断当前 WebView URL 是否命中 LINE 登录成功页。
|
|
|
+ * 后端可能带 /api 前缀,成功页命中后前端会提取 ticket 继续换取用户信息。
|
|
|
+ */
|
|
|
+function isLineLoginSuccessUrl(url) {
|
|
|
+ const text = String(url || '').toLowerCase()
|
|
|
+ return LINE_SUCCESS_PATHS.some(path => text.indexOf(path) > -1)
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * 判断当前 WebView URL 是否命中 LINE 登录失败页。
|
|
|
+ * 失败页通常会带 error_msg,前端识别后关闭 WebView 并提示用户。
|
|
|
+ */
|
|
|
+function isLineLoginFailUrl(url) {
|
|
|
+ const text = String(url || '').toLowerCase()
|
|
|
+ return LINE_FAIL_PATHS.some(path => text.indexOf(path) > -1)
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * 从 URL 查询参数中读取指定字段。
|
|
|
+ * 优先使用 URL 标准解析,兼容失败时再用正则兜底,主要用于读取 ticket/error_msg。
|
|
|
+ */
|
|
|
+function readUrlQuery(url, key) {
|
|
|
+ const target = String(url || '')
|
|
|
+ try {
|
|
|
+ const parsed = new URL(target)
|
|
|
+ return parsed.searchParams.get(key) || ''
|
|
|
+ } catch (_) {
|
|
|
+ const match = target.match(new RegExp(`[?&]${key}=([^&]+)`))
|
|
|
+ return match ? decodeURIComponent(match[1].replace(/\+/g, '%20')) : ''
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * 展开打印 /line/getuserinfo 请求失败信息。
|
|
|
+ * HttpError 会把原始响应放在 payload 中,这里把状态码、请求地址和后端响应都打印出来,便于真机排查。
|
|
|
+ */
|
|
|
+function logLineUserinfoError(err) {
|
|
|
+ // /line/getuserinfo 失败时,HttpError 的后端响应藏在 payload 里;这里展开关键字段方便真机定位。
|
|
|
+ const payload = err && err.payload
|
|
|
+ const responseBody = payload && (payload.res || payload.data)
|
|
|
+ const requestUrl = payload && payload.config && payload.config.url
|
|
|
+ console.log('[LINE 登录] /line/getuserinfo 错误详情:', {
|
|
|
+ name: err && err.name,
|
|
|
+ kind: err && err.kind,
|
|
|
+ message: err && err.message,
|
|
|
+ statusCode: payload && payload.statusCode,
|
|
|
+ errMsg: payload && payload.errMsg,
|
|
|
+ requestUrl,
|
|
|
+ responseBody
|
|
|
+ })
|
|
|
+ try {
|
|
|
+ console.log('[LINE 登录] /line/getuserinfo 后端响应 JSON:', JSON.stringify(responseBody))
|
|
|
+ } catch (_) {}
|
|
|
+}
|
|
|
+
|
|
|
/**
|
|
|
* 苹果登录(Sign in with Apple)
|
|
|
* 仅在 APP 端可用,H5 端不支持。
|
|
|
@@ -133,31 +228,33 @@ export function appleLogin() {
|
|
|
// }
|
|
|
|
|
|
/**
|
|
|
- * LINE 登录(WebView OAuth 流程)
|
|
|
- * 兼容 iOS 和 Android 端
|
|
|
+ * LINE 登录(后端授权页 WebView 流程)
|
|
|
+ * 兼容 iOS 和 Android 端。
|
|
|
*
|
|
|
- * @returns {Promise<Object>} 会话数据 { userinfo, token, ... }
|
|
|
+ * 注意:LINE 登录方式已改为先调用 GET /line/getAuthUrl 获取授权地址。
|
|
|
+ * 前端不再拼接 access.line.me OAuth 参数,也不再把 code 提交到 user/third。
|
|
|
+ *
|
|
|
+ * @returns {Promise<Object>} /line/getuserinfo 成功后返回 `{ userinfo, statistics }`,页面层沿用普通登录写入流程。
|
|
|
*/
|
|
|
export function lineLogin() {
|
|
|
- return new Promise((resolve, reject) => {
|
|
|
+ return new Promise(async (resolve, reject) => {
|
|
|
// #ifdef APP-PLUS
|
|
|
|
|
|
- console.log('==================== [LINE Login] 开始 ====================')
|
|
|
- console.log('[LINE Login] 时间:', new Date().toLocaleString())
|
|
|
- console.log('[LINE Login] Channel ID:', LINE_CHANNEL_ID)
|
|
|
- console.log('[LINE Login] Callback URL:', LINE_CALLBACK_URL)
|
|
|
-
|
|
|
- const authUrl = `https://access.line.me/oauth2/v2.1/authorize?` +
|
|
|
- `response_type=code` +
|
|
|
- `&client_id=${LINE_CHANNEL_ID}` +
|
|
|
- `&redirect_uri=${encodeURIComponent(LINE_CALLBACK_URL)}` +
|
|
|
- `&state=line_login` +
|
|
|
- `&scope=profile%20openid` +
|
|
|
- `&bot_prompt=normal`
|
|
|
+ let authUrl = ''
|
|
|
+ try {
|
|
|
+ // 先向后端获取本次 LINE 授权地址,由后端生成 state、redirect_uri 等参数。
|
|
|
+ const { data } = await get(ROUTE__LINE_AUTH_URL)
|
|
|
+ authUrl = pickLineAuthUrl(data)
|
|
|
+ } catch (err) {
|
|
|
+ reject(err)
|
|
|
+ return
|
|
|
+ }
|
|
|
+ if (!authUrl) {
|
|
|
+ reject(new Error('LINE login: no auth url received'))
|
|
|
+ return
|
|
|
+ }
|
|
|
|
|
|
- console.log('[LINE Login] 授权 URL:', authUrl)
|
|
|
-
|
|
|
- // 创建 WebView(App 内部窗口,不是外部浏览器)
|
|
|
+ // 使用 /line/getAuthUrl 返回的授权地址打开 WebView,后端负责跳转 LINE 和处理 callback。
|
|
|
const wv = plus.webview.create(authUrl, 'line-auth', {
|
|
|
top: '44px',
|
|
|
left: '0px',
|
|
|
@@ -176,52 +273,50 @@ export function lineLogin() {
|
|
|
})
|
|
|
let hasResolved = false
|
|
|
|
|
|
-
|
|
|
- // 监听页面加载
|
|
|
- wv.addEventListener('loaded', function () {
|
|
|
- const url = wv.getURL()
|
|
|
- console.log('[LINE Login] 页面加载:', url)
|
|
|
-
|
|
|
- // 检查是否跳转到回调 URL 或包含 code 参数
|
|
|
- if (url.indexOf(LINE_CALLBACK_URL) === 0 || url.indexOf('code=') > -1) {
|
|
|
+ // 监听页面流转;命中后端登录成功页时,提取 ticket 再换取 App 登录态。
|
|
|
+ wv.addEventListener('loaded', async function () {
|
|
|
+ const url = readWebviewUrl(wv)
|
|
|
+ if (isLineLoginFailUrl(url)) {
|
|
|
if (hasResolved) return
|
|
|
hasResolved = true
|
|
|
- // 解析 code
|
|
|
- const code = extractCodeFromUrl(url)
|
|
|
- // 关闭 WebView
|
|
|
+ // 后端失败页会携带 error_msg,前端识别后关闭 WebView 并把原因抛给页面层。
|
|
|
+ const message = readUrlQuery(url, 'error_msg') || readUrlQuery(url, 'message') || 'LINE login failed'
|
|
|
wv.close()
|
|
|
+ reject(new Error(message))
|
|
|
+ return
|
|
|
+ }
|
|
|
+ if (!isLineLoginSuccessUrl(url)) return
|
|
|
+ if (hasResolved) return
|
|
|
|
|
|
- if (!code) {
|
|
|
- console.error('[LINE Login] ❌ 未获取到 authorization code')
|
|
|
- console.error('[LINE Login] URL:', url)
|
|
|
- reject(new Error('LINE login: no code received'))
|
|
|
- return
|
|
|
- }
|
|
|
+ hasResolved = true
|
|
|
+ const ticket = readUrlQuery(url, 'ticket')
|
|
|
+ // ticket 属于登录凭证,只用于换取用户信息,不输出到日志。
|
|
|
|
|
|
- console.log('[LINE Login] ✅ 获取到 code:', code)
|
|
|
- console.log('[LINE Login] 准备调用后端')
|
|
|
-
|
|
|
- // 发送到后端验证
|
|
|
- post(ROUTE__LOGIN, {
|
|
|
- platform: 'line',
|
|
|
- authorizationCode: code,
|
|
|
- client_id: readClientHandle()
|
|
|
- }).then(({ data }) => {
|
|
|
- console.log('[LINE Login] ✅ 后端接口返回成功')
|
|
|
- console.log('[LINE Login] 后端返回数据:', JSON.stringify(data, null, 2))
|
|
|
- console.log('==================== [LINE Login] 完成 ====================')
|
|
|
- resolve(unwrapSession(data))
|
|
|
- }).catch(err => {
|
|
|
- console.error('[LINE Login] ❌ 后端请求失败', err)
|
|
|
- reject(err)
|
|
|
+ if (!ticket) {
|
|
|
+ wv.close()
|
|
|
+ reject(new Error('LINE login: missing ticket'))
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ // 后端接口按表单参数读取 ticket;data 保持对象形态,避免影响项目 Sign 签名计算。
|
|
|
+ const { data } = await post(ROUTE__LINE_GET_USERINFO, { ticket }, {
|
|
|
+ header: FORM_URLENCODED_HEADER
|
|
|
})
|
|
|
+ // 用户信息里包含 token 等敏感字段,成功时不打印响应内容。
|
|
|
+ wv.close()
|
|
|
+ resolve(unwrapSession(data))
|
|
|
+ } catch (err) {
|
|
|
+ logLineUserinfoError(err)
|
|
|
+ wv.close()
|
|
|
+ reject(err)
|
|
|
}
|
|
|
})
|
|
|
|
|
|
// 监听错误
|
|
|
wv.addEventListener('error', function (error) {
|
|
|
- console.log('[LINE Login] ❌ WebView 错误:', JSON.stringify(error))
|
|
|
- console.log('[LINE Login] 错误详情:', error)
|
|
|
+ // WebView 加载错误保留日志,便于定位授权页无法打开等异常。
|
|
|
+ console.log('[LINE 登录][WebView] 加载错误:', error)
|
|
|
if (!hasResolved) {
|
|
|
hasResolved = true
|
|
|
wv.close()
|
|
|
@@ -239,17 +334,3 @@ export function lineLogin() {
|
|
|
// #endif
|
|
|
})
|
|
|
}
|
|
|
-
|
|
|
-/**
|
|
|
- * 从 URL 中提取 authorization code
|
|
|
- */
|
|
|
-function extractCodeFromUrl(url) {
|
|
|
- try {
|
|
|
- const urlObj = new URL(url)
|
|
|
- return urlObj.searchParams.get('code')
|
|
|
- } catch (e) {
|
|
|
- // 手动解析
|
|
|
- const match = url.match(/[?&]code=([^&]+)/)
|
|
|
- return match ? match[1] : null
|
|
|
- }
|
|
|
-}
|