feat: complete admin API integration
This commit is contained in:
+79
-26
@@ -1,8 +1,13 @@
|
||||
import { computed, ref } from 'vue'
|
||||
import { defineStore } from 'pinia'
|
||||
import type { CurrentUser } from '@chongchong/shared'
|
||||
import { http, registerAuthRefreshHandler, unwrapApiResponse, type ApiEnvelope } from '@/api/http'
|
||||
import { listPlatformPermissions, type PlatformPermissionResource } from '@/api/platformIam'
|
||||
import {
|
||||
http,
|
||||
registerAuthRefreshHandler,
|
||||
unwrapApiResponse,
|
||||
type ApiEnvelope,
|
||||
} from '@/api/http'
|
||||
import { listCurrentPlatformPermissionCodes, type PlatformPermissionResource } from '@/api/platformIam'
|
||||
import { decodeJwtClaims } from '@/utils/jwt'
|
||||
|
||||
const TOKEN_KEY = 'cc-admin-token'
|
||||
@@ -17,10 +22,13 @@ interface LoginResponse {
|
||||
refreshExpiresIn: number
|
||||
}
|
||||
|
||||
class ExpiredAccessTokenError extends Error {}
|
||||
class SupersededSessionError extends Error {}
|
||||
|
||||
function sessionFromToken(token: string) {
|
||||
const claims = decodeJwtClaims(token)
|
||||
if (claims.app_code !== 'PLATFORM') throw new Error('账号不属于当前应用端')
|
||||
if (claims.exp && claims.exp * 1000 <= Date.now()) throw new Error('登录状态已过期,请重新登录')
|
||||
if (claims.exp && claims.exp * 1000 <= Date.now()) throw new ExpiredAccessTokenError('登录状态已过期')
|
||||
return {
|
||||
user: {
|
||||
id: String(claims.user_id ?? ''),
|
||||
@@ -39,6 +47,7 @@ export const useAuthStore = defineStore('admin-auth', () => {
|
||||
const permissions = ref<string[]>([])
|
||||
const permissionResources = ref<PlatformPermissionResource[]>([])
|
||||
const isServerSession = ref(false)
|
||||
let sessionGeneration = 0
|
||||
|
||||
if (token.value) {
|
||||
try {
|
||||
@@ -48,20 +57,34 @@ export const useAuthStore = defineStore('admin-auth', () => {
|
||||
user.value = session.user
|
||||
permissions.value = session.permissions.filter((code) => cachedPermissions.includes(code))
|
||||
isServerSession.value = true
|
||||
} catch {
|
||||
} catch (error) {
|
||||
token.value = ''
|
||||
refreshToken.value = ''
|
||||
localStorage.removeItem(TOKEN_KEY)
|
||||
localStorage.removeItem(REFRESH_TOKEN_KEY)
|
||||
localStorage.removeItem(PERMISSION_CODES_KEY)
|
||||
if (!(error instanceof ExpiredAccessTokenError) || !refreshToken.value) {
|
||||
refreshToken.value = ''
|
||||
localStorage.removeItem(REFRESH_TOKEN_KEY)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const isAuthenticated = computed(() => Boolean(token.value))
|
||||
const permissionCodes = computed<ReadonlySet<string>>(() => new Set(permissions.value))
|
||||
|
||||
async function applySession(result: LoginResponse) {
|
||||
function ensureCurrentGeneration(expectedGeneration: number) {
|
||||
if (expectedGeneration !== sessionGeneration) {
|
||||
throw new SupersededSessionError('登录会话已被后续操作替代')
|
||||
}
|
||||
}
|
||||
|
||||
function applyPermissionCodes(codes: string[]) {
|
||||
permissions.value = codes
|
||||
localStorage.setItem(PERMISSION_CODES_KEY, JSON.stringify(codes))
|
||||
}
|
||||
|
||||
async function applySession(result: LoginResponse, expectedGeneration: number) {
|
||||
const session = sessionFromToken(result.accessToken)
|
||||
ensureCurrentGeneration(expectedGeneration)
|
||||
token.value = result.accessToken
|
||||
refreshToken.value = result.refreshToken
|
||||
user.value = session.user
|
||||
@@ -69,19 +92,19 @@ export const useAuthStore = defineStore('admin-auth', () => {
|
||||
localStorage.setItem(TOKEN_KEY, token.value)
|
||||
localStorage.setItem(REFRESH_TOKEN_KEY, refreshToken.value)
|
||||
try {
|
||||
permissionResources.value = await listPlatformPermissions()
|
||||
const activeResourceCodes = new Set(permissionResources.value
|
||||
.filter((resource) => resource.status === 'ACTIVE')
|
||||
.map((resource) => resource.permissionCode))
|
||||
permissions.value = session.permissions.filter((code) => activeResourceCodes.has(code))
|
||||
localStorage.setItem(PERMISSION_CODES_KEY, JSON.stringify(permissions.value))
|
||||
const activePermissionCodes = await listCurrentPlatformPermissionCodes()
|
||||
ensureCurrentGeneration(expectedGeneration)
|
||||
permissionResources.value = []
|
||||
const activePermissionCodeSet = new Set(activePermissionCodes)
|
||||
applyPermissionCodes(session.permissions.filter((code) => activePermissionCodeSet.has(code)))
|
||||
} catch (error) {
|
||||
clearSession()
|
||||
ensureCurrentGeneration(expectedGeneration)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
function clearSession() {
|
||||
sessionGeneration += 1
|
||||
token.value = ''
|
||||
refreshToken.value = ''
|
||||
user.value = null
|
||||
@@ -93,44 +116,73 @@ export const useAuthStore = defineStore('admin-auth', () => {
|
||||
localStorage.removeItem(PERMISSION_CODES_KEY)
|
||||
}
|
||||
|
||||
async function revokeTokenPair(accessToken: string, refreshTokenToRevoke: string) {
|
||||
await http.post('/auth/logout', { refreshToken: refreshTokenToRevoke }, {
|
||||
headers: { Authorization: `Bearer ${accessToken}` },
|
||||
})
|
||||
}
|
||||
|
||||
async function login(account: string, password: string) {
|
||||
const response = await http.post<ApiEnvelope<LoginResponse>, ApiEnvelope<LoginResponse>>(
|
||||
'/auth/login',
|
||||
{ username: account, password },
|
||||
)
|
||||
const result = unwrapApiResponse<LoginResponse>(response)
|
||||
await applySession(result)
|
||||
const expectedGeneration = ++sessionGeneration
|
||||
try {
|
||||
const response = await http.post<ApiEnvelope<LoginResponse>, ApiEnvelope<LoginResponse>>(
|
||||
'/auth/login',
|
||||
{ username: account, password },
|
||||
)
|
||||
const result = unwrapApiResponse<LoginResponse>(response)
|
||||
await applySession(result, expectedGeneration)
|
||||
} catch (error) {
|
||||
if (expectedGeneration === sessionGeneration) {
|
||||
const issuedAccessToken = token.value
|
||||
const issuedRefreshToken = refreshToken.value
|
||||
clearSession()
|
||||
if (issuedAccessToken && issuedRefreshToken) {
|
||||
void revokeTokenPair(issuedAccessToken, issuedRefreshToken).catch(() => undefined)
|
||||
}
|
||||
}
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshSession() {
|
||||
if (!refreshToken.value) throw new Error('刷新令牌不存在,请重新登录')
|
||||
const expectedGeneration = sessionGeneration
|
||||
try {
|
||||
const response = await http.post<ApiEnvelope<LoginResponse>, ApiEnvelope<LoginResponse>>(
|
||||
'/auth/refresh',
|
||||
{ refreshToken: refreshToken.value },
|
||||
)
|
||||
const result = unwrapApiResponse<LoginResponse>(response)
|
||||
await applySession(result)
|
||||
await applySession(result, expectedGeneration)
|
||||
} catch (error) {
|
||||
clearSession()
|
||||
if (expectedGeneration === sessionGeneration) clearSession()
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
async function restoreSession() {
|
||||
if (isAuthenticated.value) return true
|
||||
if (!refreshToken.value) return false
|
||||
try {
|
||||
await refreshSession()
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
registerAuthRefreshHandler(async () => {
|
||||
await refreshSession()
|
||||
if (!token.value) throw new Error('登录状态刷新失败')
|
||||
return token.value
|
||||
})
|
||||
|
||||
function logout() {
|
||||
async function logout() {
|
||||
const accessToken = token.value
|
||||
const tokenToRevoke = refreshToken.value
|
||||
clearSession()
|
||||
if (accessToken && tokenToRevoke) {
|
||||
void http.post('/auth/logout', { refreshToken: tokenToRevoke }, {
|
||||
headers: { Authorization: `Bearer ${accessToken}` },
|
||||
}).catch(() => undefined)
|
||||
await revokeTokenPair(accessToken, tokenToRevoke)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -145,6 +197,7 @@ export const useAuthStore = defineStore('admin-auth', () => {
|
||||
isAuthenticated,
|
||||
login,
|
||||
refreshSession,
|
||||
restoreSession,
|
||||
logout,
|
||||
}
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user