Files
EchoChat/frontend/src/pages/auth/sso.vue
2026-05-24 00:00:56 +08:00

212 lines
6.7 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!--
外部系统 SSO 桥接页cloud_law -> EchoChat
入参query
- payload: URI 编码的 JSON 字符串 { token, refresh_token, expires_in, user }
- redirect: URI 编码的目标 hash 路径例如 /pages/meeting/join?code=XXX-XXX-XXX
逻辑
1. 读取 payload写入 storagesaveToken + saveUserInfo
2. 同步 user store
3. 跳转 redirect默认 /pages/meeting/index
-->
<template>
<view class="sso">
<!-- 仅出错时显示文字正常跳转过程肉眼无感背景与会议页一致 -->
<text v-if="hasError" class="sso-title">{{ statusText }}</text>
</view>
</template>
<script setup>
import { ref } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import { saveToken, saveUserInfo } from '@/utils/storage'
import { useUserStore } from '@/store/user'
import { useWebSocketStore } from '@/store/websocket'
import { useChatStore } from '@/store/chat'
import { useContactStore } from '@/store/contact'
import { useGroupStore } from '@/store/group'
import { useNotifyStore } from '@/store/notify'
const statusText = ref('正在登录…')
const hasError = ref(false)
function safeDecode(value) {
try {
return decodeURIComponent(value)
} catch (e) {
return value
}
}
function readRawQueryParams() {
const result = {}
try {
if (typeof window === 'undefined') return result
const search = String(window.location.search || '').replace(/^\?/, '')
const hash = String(window.location.hash || '')
const hashQuery = hash.indexOf('?') >= 0 ? hash.slice(hash.indexOf('?') + 1) : ''
;[search, hashQuery].forEach(raw => {
if (!raw) return
const params = new URLSearchParams(raw)
params.forEach((value, key) => {
if (result[key] === undefined) {
result[key] = value
}
})
})
} catch (e) {}
return result
}
function readSsoParam(query, key) {
const value = query && query[key] !== undefined ? query[key] : readRawQueryParams()[key]
return value === undefined || value === null ? '' : String(value)
}
function resolveSsoPayload(query) {
const payloadText = readSsoParam(query, 'payload')
if (payloadText) {
return JSON.parse(safeDecode(payloadText))
}
const token = readSsoParam(query, 'token')
const refreshToken = readSsoParam(query, 'refresh_token')
const expiresIn = Number(readSsoParam(query, 'expires_in') || 0)
const userText = readSsoParam(query, 'user')
const user = userText ? JSON.parse(safeDecode(userText)) : null
return { token, refresh_token: refreshToken, expires_in: expiresIn, user }
}
function decodeJwtPayload(token) {
try {
const parts = String(token || '').split('.')
if (parts.length < 2) return null
let base64 = parts[1].replace(/-/g, '+').replace(/_/g, '/')
while (base64.length % 4) base64 += '='
const binary = atob(base64)
const json = decodeURIComponent(Array.prototype.map.call(binary, c => {
return `%${(`00${c.charCodeAt(0).toString(16)}`).slice(-2)}`
}).join(''))
return JSON.parse(json)
} catch (e) {
return null
}
}
function resolveSsoUser(payload) {
if (payload && payload.user) return payload.user
const claims = decodeJwtPayload(payload && payload.token)
if (!claims || !claims.user_id) return null
const username = claims.username || String(claims.user_id)
return {
id: claims.user_id,
user_id: claims.user_id,
username,
nickname: username,
roles: Array.isArray(claims.roles) ? claims.roles : []
}
}
function resolveRedirect(query) {
const rawRedirect = readSsoParam(query || {}, 'redirect')
const decoded = rawRedirect ? safeDecode(rawRedirect) : '/pages/meeting/index'
const cleaned = String(decoded || '').trim().replace(/^["']+|["']+$/g, '')
return cleaned || '/pages/meeting/index'
}
function markCloudLawEmbed() {
try {
if (typeof window === 'undefined') return
window.sessionStorage.setItem('echo_cloud_law_meeting_embed', '1')
} catch (e) {}
}
function bootstrapAfterSso(token) {
// 模拟 App.onLaunch 的初始化路径:先重连 WS顶掉旧 token 连接),再注册各 store 的 ws 监听并拉初始数据
try {
const wsStore = useWebSocketStore()
wsStore.disconnect()
wsStore.connect(token)
} catch (wsErr) {
console.warn('[SSO] WebSocket 重连失败', wsErr)
}
try {
const chatStore = useChatStore()
const contactStore = useContactStore()
const groupStore = useGroupStore()
const notifyStore = useNotifyStore()
chatStore.initWsListeners()
contactStore.initWsListeners()
groupStore.initWsListeners()
notifyStore.initWsListeners()
contactStore.fetchPendingRequests().catch(() => {})
notifyStore.fetchUnreadCount().catch(() => {})
} catch (storeErr) {
console.warn('[SSO] 全局 store 初始化异常', storeErr)
}
}
onLoad((query) => {
try {
try {
console.log('[SSO] location:', {
search: typeof window !== 'undefined' ? window.location.search : '',
hash: typeof window !== 'undefined' ? window.location.hash : '',
query
})
} catch (logErr) {}
const payload = resolveSsoPayload(query || {})
const ssoUser = resolveSsoUser(payload)
const redirect = resolveRedirect(query || {})
console.log('[SSO] resolved params:', {
hasToken: !!(payload && payload.token),
hasPayloadUser: !!(payload && payload.user),
hasSsoUser: !!ssoUser,
userId: ssoUser && (ssoUser.id || ssoUser.user_id),
redirect
})
if (!payload || !payload.token || !ssoUser) {
statusText.value = '缺少 payload 参数'
hasError.value = true
return
}
markCloudLawEmbed()
// 写入持久化存储
saveToken(payload.token, payload.refresh_token || '', payload.expires_in || 0)
saveUserInfo(ssoUser)
// 同步 pinia store
const userStore = useUserStore()
userStore.token = payload.token
userStore.userInfo = ssoUser
bootstrapAfterSso(payload.token)
statusText.value = '登录成功,跳转中…'
console.log('[SSO] redirectTo:', redirect)
// redirectTo 不保留历史,避免返回到 sso 页造成循环
// 此前曾延迟 200ms 跳转,导致拉起会议时中间出现白底"正在登录…"过渡页;
// saveToken/saveUserInfo 已是同步 storage 写入,无需额外等待。
uni.redirectTo({ url: redirect })
} catch (e) {
console.error('[SSO] 解析失败', e)
statusText.value = `SSO 失败:${e.message || e}`
hasError.value = true
}
})
</script>
<style scoped>
.sso {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
/* 与进入后的会议页背景一致,跳转闪烁不可见 */
background: #050b1d;
}
.sso-title {
font-size: 30rpx;
color: rgba(255,255,255,0.78);
}
</style>