修复冻结用户即时下线
This commit is contained in:
@@ -427,6 +427,9 @@ func New(cfg config.Config, deps Dependencies, logger *zap.Logger) *gin.Engine {
|
|||||||
authRoutes.POST("/password/reset", authHandler.ResetPassword)
|
authRoutes.POST("/password/reset", authHandler.ResetPassword)
|
||||||
authRoutes.POST("/refresh", authHandler.Refresh)
|
authRoutes.POST("/refresh", authHandler.Refresh)
|
||||||
authRoutes.POST("/logout", authHandler.Logout)
|
authRoutes.POST("/logout", authHandler.Logout)
|
||||||
|
if chatHubHandler != nil {
|
||||||
|
authRoutes.GET("/events", requireAuth, chatHubHandler.UserEvents)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
api.GET("/me", requireAuth, userHandler.Me)
|
api.GET("/me", requireAuth, userHandler.Me)
|
||||||
|
|||||||
@@ -5,10 +5,12 @@ import { computed } from 'vue'
|
|||||||
import { RouterView, useRoute } from 'vue-router'
|
import { RouterView, useRoute } from 'vue-router'
|
||||||
|
|
||||||
import InAppBrowserPrompt from './components/InAppBrowserPrompt.vue'
|
import InAppBrowserPrompt from './components/InAppBrowserPrompt.vue'
|
||||||
|
import { useSessionMonitor } from './features/auth/composables/useSessionMonitor'
|
||||||
import AdminLayout from './layouts/AdminLayout.vue'
|
import AdminLayout from './layouts/AdminLayout.vue'
|
||||||
import AppLayout from './layouts/AppLayout.vue'
|
import AppLayout from './layouts/AppLayout.vue'
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
|
useSessionMonitor()
|
||||||
const layout = computed(() => {
|
const layout = computed(() => {
|
||||||
if (route.path === '/m' || route.path.startsWith('/m/')) {
|
if (route.path === '/m' || route.path.startsWith('/m/')) {
|
||||||
return 'blank'
|
return 'blank'
|
||||||
|
|||||||
@@ -80,8 +80,8 @@ export async function resetPassword(phone: string, code: string, newPassword: st
|
|||||||
await apiClient.post('/auth/password/reset', { phone, code, new_password: newPassword })
|
await apiClient.post('/auth/password/reset', { phone, code, new_password: newPassword })
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function fetchMe() {
|
export async function fetchMe(options?: { silent?: boolean }) {
|
||||||
const { data } = await apiClient.get<ApiResponse<AuthUser>>('/me')
|
const { data } = await apiClient.get<ApiResponse<AuthUser>>('/me', options)
|
||||||
return data.data
|
return data.data
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,117 @@
|
|||||||
|
import { onBeforeUnmount, onMounted, watch } from 'vue'
|
||||||
|
|
||||||
|
import { fetchMe } from '@/features/auth/api/auth'
|
||||||
|
import { getAccessToken, type AuthScope } from '@/shared/utils/authStorage'
|
||||||
|
import { useSessionStore } from '@/stores/session'
|
||||||
|
|
||||||
|
const scope: AuthScope = 'user'
|
||||||
|
const reconnectDelay = 3000
|
||||||
|
const verificationInterval = 5000
|
||||||
|
|
||||||
|
// 保持一个轻量级会话通道,使后台冻结能在没有页面请求时立即让用户退出。
|
||||||
|
export function useSessionMonitor() {
|
||||||
|
const session = useSessionStore()
|
||||||
|
let source: EventSource | null = null
|
||||||
|
let reconnectTimer: number | null = null
|
||||||
|
let verificationTimer: number | null = null
|
||||||
|
let checking = false
|
||||||
|
let stopped = false
|
||||||
|
|
||||||
|
function closeSource() {
|
||||||
|
source?.close()
|
||||||
|
source = null
|
||||||
|
}
|
||||||
|
|
||||||
|
function scheduleReconnect() {
|
||||||
|
if (stopped || reconnectTimer !== null || !session.isLoggedIn) return
|
||||||
|
reconnectTimer = window.setTimeout(() => {
|
||||||
|
reconnectTimer = null
|
||||||
|
connect()
|
||||||
|
}, reconnectDelay)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function verifySession() {
|
||||||
|
if (stopped || checking || !session.isLoggedIn) return
|
||||||
|
checking = true
|
||||||
|
try {
|
||||||
|
await fetchMe({ silent: true })
|
||||||
|
} catch {
|
||||||
|
// 认证失败由 axios 拦截器清理令牌并跳转;网络错误等待下一次校验。
|
||||||
|
} finally {
|
||||||
|
checking = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function connect() {
|
||||||
|
if (stopped || source || !session.isLoggedIn) return
|
||||||
|
const token = getAccessToken(scope)
|
||||||
|
if (!token) return
|
||||||
|
|
||||||
|
source = new EventSource(`/api/auth/events?token=${encodeURIComponent(token)}`, {
|
||||||
|
withCredentials: true,
|
||||||
|
})
|
||||||
|
source.addEventListener('connected', () => undefined)
|
||||||
|
source.onerror = () => {
|
||||||
|
closeSource()
|
||||||
|
void verifySession()
|
||||||
|
scheduleReconnect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function stop() {
|
||||||
|
closeSource()
|
||||||
|
if (reconnectTimer !== null) {
|
||||||
|
window.clearTimeout(reconnectTimer)
|
||||||
|
reconnectTimer = null
|
||||||
|
}
|
||||||
|
if (verificationTimer !== null) {
|
||||||
|
window.clearInterval(verificationTimer)
|
||||||
|
verificationTimer = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function start() {
|
||||||
|
stop()
|
||||||
|
if (!session.isLoggedIn) return
|
||||||
|
connect()
|
||||||
|
void verifySession()
|
||||||
|
verificationTimer = window.setInterval(verifySession, verificationInterval)
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleAuthStorageChanged(event: Event) {
|
||||||
|
const detail = (event as CustomEvent<{ scope?: AuthScope }>).detail
|
||||||
|
if (detail?.scope !== scope || stopped) return
|
||||||
|
if (session.isLoggedIn) {
|
||||||
|
start()
|
||||||
|
} else {
|
||||||
|
stop()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleVisibilityChange() {
|
||||||
|
if (document.visibilityState === 'visible') void verifySession()
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
stopped = false
|
||||||
|
start()
|
||||||
|
window.addEventListener('auth-storage-changed', handleAuthStorageChanged)
|
||||||
|
document.addEventListener('visibilitychange', handleVisibilityChange)
|
||||||
|
})
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
stopped = true
|
||||||
|
stop()
|
||||||
|
window.removeEventListener('auth-storage-changed', handleAuthStorageChanged)
|
||||||
|
document.removeEventListener('visibilitychange', handleVisibilityChange)
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => session.isLoggedIn,
|
||||||
|
loggedIn => {
|
||||||
|
if (stopped) return
|
||||||
|
if (loggedIn) start()
|
||||||
|
else stop()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user