feat(auth): 添加登出功能并优化认证处理
- 添加了登出相关的API端点和常量定义 - 实现了登出功能及密码验证登出逻辑 - 添加了登出会话清理和浏览器存储清除 - 在用户信息模态框中集成了登出按钮 - 添加了登出相关的国际化翻译 - 优化了API客户端中的认证错误处理 - 实现了无效令牌的自动处理机制 - 更新了GitNexus索引统计数据 - 修改了构建输出目录配置 - 清理了不必要的注释和代码 - 调整了移动端头部组件结构 - 优化了游戏历史记录查询逻辑 - 添加了控制台日志用于调试 - 设置默认注册邀请码为D97DBC16 - 在.gitignore中添加构建产物忽略规则
This commit is contained in:
@@ -2,6 +2,7 @@ import ky, { HTTPError, type Options, TimeoutError } from 'ky'
|
||||
import {
|
||||
ACCESS_TOKEN_REFRESH_SKEW_MS,
|
||||
API_ERROR_MESSAGES,
|
||||
AUTH_INVALID_TOKEN_CODE,
|
||||
AUTH_REFRESH_ATTEMPTED_CONTEXT_KEY,
|
||||
AUTH_REFRESH_ENDPOINT,
|
||||
AUTH_SKIP_REFRESH_CONTEXT_KEY,
|
||||
@@ -14,6 +15,7 @@ import type { AuthTokenDto } from '@/features/auth/api/types'
|
||||
import { getPreferredLanguage, isSupportedLanguage } from '@/i18n'
|
||||
import { ApiError } from '@/lib/api/api-error.ts'
|
||||
import {
|
||||
handleInvalidTokenSession,
|
||||
handleUnauthorizedSession,
|
||||
tryRefreshAuthSession,
|
||||
} from '@/lib/auth/auth-session'
|
||||
@@ -90,6 +92,10 @@ function getErrorMessage(response: Response, data: unknown) {
|
||||
}
|
||||
|
||||
async function toApiError(error: unknown) {
|
||||
if (error instanceof ApiError) {
|
||||
return error
|
||||
}
|
||||
|
||||
if (error instanceof HTTPError) {
|
||||
const data = error.data
|
||||
|
||||
@@ -186,7 +192,40 @@ function shouldTryRefreshAccessToken(input: string, options?: Options) {
|
||||
)
|
||||
}
|
||||
|
||||
function isApiEnvelope(value: unknown): value is ApiResponse<unknown> {
|
||||
return Boolean(
|
||||
value &&
|
||||
typeof value === 'object' &&
|
||||
'code' in value &&
|
||||
typeof value.code === 'number',
|
||||
)
|
||||
}
|
||||
|
||||
function getApiEnvelopeMessage(response: ApiResponse<unknown>) {
|
||||
return 'msg' in response && typeof response.msg === 'string'
|
||||
? response.msg
|
||||
: 'message' in response && typeof response.message === 'string'
|
||||
? response.message
|
||||
: API_ERROR_MESSAGES.unexpected
|
||||
}
|
||||
|
||||
function assertValidAuthEnvelope(data: unknown) {
|
||||
if (!isApiEnvelope(data) || data.code !== AUTH_INVALID_TOKEN_CODE) {
|
||||
return
|
||||
}
|
||||
|
||||
handleInvalidTokenSession()
|
||||
|
||||
throw new ApiError({
|
||||
data,
|
||||
message: getApiEnvelopeMessage(data),
|
||||
status: 401,
|
||||
})
|
||||
}
|
||||
|
||||
function unwrapEnvelopeData<T>(response: ApiResponse<T>) {
|
||||
assertValidAuthEnvelope(response)
|
||||
|
||||
if (response.code === 1) {
|
||||
return response.data
|
||||
}
|
||||
@@ -313,6 +352,8 @@ async function request<TResponse>(input: string, options?: Options) {
|
||||
)
|
||||
const data = await parseResponseBody(response)
|
||||
|
||||
assertValidAuthEnvelope(data)
|
||||
|
||||
return data as TResponse
|
||||
} catch (error) {
|
||||
if (
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import i18n from '@/i18n'
|
||||
import { notify } from '@/lib/notify'
|
||||
import { queryClient } from '@/lib/query/query-client'
|
||||
import type { AuthSessionInput, AuthUser } from '@/store/auth'
|
||||
import { useAuthStore } from '@/store/auth'
|
||||
import { useModalStore } from '@/store/modal'
|
||||
|
||||
export type CurrentUserInitializer = () => Promise<AuthUser | null>
|
||||
export type RefreshSessionHandler = (
|
||||
@@ -10,6 +14,28 @@ let currentUserInitializer: CurrentUserInitializer | null = null
|
||||
let refreshSessionHandler: RefreshSessionHandler | null = null
|
||||
let authInitializationPromise: Promise<void> | null = null
|
||||
let refreshSessionPromise: Promise<boolean> | null = null
|
||||
let lastLoginPromptAt = 0
|
||||
|
||||
const LOGIN_PROMPT_DEDUP_MS = 1200
|
||||
|
||||
interface ClearAuthenticatedSessionOptions {
|
||||
clearBrowserStorage?: boolean
|
||||
}
|
||||
|
||||
interface UnauthorizedSessionOptions extends ClearAuthenticatedSessionOptions {
|
||||
openLoginModal?: boolean
|
||||
showLoginRequiredToast?: boolean
|
||||
}
|
||||
|
||||
function clearBrowserStorageData() {
|
||||
if (typeof localStorage !== 'undefined') {
|
||||
localStorage.clear()
|
||||
}
|
||||
|
||||
if (typeof sessionStorage !== 'undefined') {
|
||||
sessionStorage.clear()
|
||||
}
|
||||
}
|
||||
|
||||
export function registerCurrentUserInitializer(
|
||||
initializer: CurrentUserInitializer | null,
|
||||
@@ -29,8 +55,55 @@ export function isAuthenticated() {
|
||||
return snapshot.status === 'authenticated' && Boolean(snapshot.accessToken)
|
||||
}
|
||||
|
||||
export function handleUnauthorizedSession() {
|
||||
export function clearAuthenticatedSession({
|
||||
clearBrowserStorage = true,
|
||||
}: ClearAuthenticatedSessionOptions = {}) {
|
||||
useAuthStore.getState().markUnauthorized()
|
||||
queryClient.clear()
|
||||
|
||||
if (clearBrowserStorage) {
|
||||
clearBrowserStorageData()
|
||||
}
|
||||
}
|
||||
|
||||
export function handleUnauthorizedSession({
|
||||
clearBrowserStorage = false,
|
||||
openLoginModal = false,
|
||||
showLoginRequiredToast = false,
|
||||
}: UnauthorizedSessionOptions = {}) {
|
||||
clearAuthenticatedSession({ clearBrowserStorage })
|
||||
|
||||
if (!openLoginModal && !showLoginRequiredToast) {
|
||||
return
|
||||
}
|
||||
|
||||
const now = Date.now()
|
||||
const shouldPrompt = now - lastLoginPromptAt > LOGIN_PROMPT_DEDUP_MS
|
||||
|
||||
if (!shouldPrompt) {
|
||||
return
|
||||
}
|
||||
|
||||
lastLoginPromptAt = now
|
||||
|
||||
if (showLoginRequiredToast) {
|
||||
notify.warning(i18n.t('commonUi.toast.loginRequired'))
|
||||
}
|
||||
|
||||
if (openLoginModal) {
|
||||
const modalStore = useModalStore.getState()
|
||||
|
||||
modalStore.closeAllModals()
|
||||
modalStore.setModalOpen('desktopLogin', true)
|
||||
}
|
||||
}
|
||||
|
||||
export function handleInvalidTokenSession() {
|
||||
handleUnauthorizedSession({
|
||||
clearBrowserStorage: true,
|
||||
openLoginModal: true,
|
||||
showLoginRequiredToast: true,
|
||||
})
|
||||
}
|
||||
|
||||
export async function initializeAuthSession() {
|
||||
|
||||
Reference in New Issue
Block a user