feat: 支持平台代登录客户工作台

This commit is contained in:
2026-08-08 10:14:07 +08:00
parent 0b2380689c
commit c4540c1c0c
7 changed files with 85 additions and 6 deletions
+10
View File
@@ -21,6 +21,12 @@ export interface RegisterMerchantResult {
nextStep: string
}
export interface ImpersonationExchangeResult {
tokenType: string
accessToken: string
expiresIn: number
}
export function loginMerchant(username: string, password: string) {
return apiRequest<TokenPair>({ method: 'POST', url: '/auth/login', data: { username, password } })
}
@@ -36,3 +42,7 @@ export function logoutMerchant(refreshToken: string) {
export function changeCurrentPassword(oldPassword: string, newPassword: string) {
return apiRequest<void>({ method: 'PUT', url: '/auth/password', data: { oldPassword, newPassword } })
}
export function exchangeImpersonationCode(code: string) {
return apiRequest<ImpersonationExchangeResult>({ method: 'POST', url: '/auth/impersonations/exchange', data: { code } })
}
+33 -3
View File
@@ -1,7 +1,7 @@
import { computed, ref } from 'vue'
import { defineStore } from 'pinia'
import type { CurrentUser } from '@chongchong/shared'
import { loginMerchant, logoutMerchant } from '@/modules/auth/api/auth'
import { exchangeImpersonationCode, loginMerchant, logoutMerchant } from '@/modules/auth/api/auth'
import { ApiError } from '@/api/http'
import {
FRONTEND_TO_MERCHANT_PERMISSION_CODES,
@@ -12,7 +12,7 @@ import {
updateMerchantPersonalProfile,
type MerchantPermissionResource,
} from '@/modules/tenant-management/public'
import { clearSessionTokens, getAccessToken, getRefreshToken, persistTokenPair } from '@/api/session'
import { clearSessionTokens, getAccessToken, getRefreshToken, persistTemporaryAccessToken, persistTokenPair } from '@/api/session'
import { decodeMerchantToken, type MerchantJwtPayload } from '@/modules/auth/utils/jwt'
const frontendPermissionCodes = new Set(TENANT_PERMISSION_GROUPS.flatMap((group) => group.items.map((item) => item.code)))
@@ -188,6 +188,27 @@ export const useAuthStore = defineStore('client-auth', () => {
}
}
async function acceptImpersonationCode(code: string) {
const exchanged = await exchangeImpersonationCode(code)
const decoded = decodeMerchantToken(exchanged.accessToken)
persistTemporaryAccessToken(exchanged.accessToken)
token.value = exchanged.accessToken
payload.value = decoded
user.value = currentUser(decoded)
profile.value = { ...emptyProfile, username: decoded.username, realName: decoded.username }
permissionCodes.value = mapPermissions(decoded)
sessionRestored.value = false
try {
await loadSessionContext()
} catch (error) {
if (isExpiredSessionError(error)) {
clearSession()
throw error
}
}
sessionRestored.value = true
}
async function logout() {
const refreshToken = getRefreshToken()
try {
@@ -220,9 +241,18 @@ export const useAuthStore = defineStore('client-auth', () => {
}
window.addEventListener('cc-client-session-expired', clearSession)
window.addEventListener('message', (event: MessageEvent<unknown>) => {
const configuredAdminOrigin = String(import.meta.env.VITE_ADMIN_WEB_ORIGIN || '').trim()
const expectedAdminOrigin = configuredAdminOrigin || window.location.origin
const message = event.data as { source?: string; type?: string; code?: string } | null
if (event.source !== window.opener || event.origin !== expectedAdminOrigin || message?.source !== 'chongchong-admin' || message.type !== 'impersonation-ticket' || !message.code) return
void acceptImpersonationCode(message.code)
.then(() => window.location.replace('/'))
.catch(() => window.location.replace('/login?impersonation=failed'))
})
window.addEventListener('storage', (event) => {
if (event.key === 'cc-client-access-token' || event.key === 'cc-client-refresh-token') applyStoredSession()
})
return { token, user, profile, permissionCodes, isAuthenticated, sessionRestored, hasPermission, login, logout, restoreSession, updateProfile, savePersonalProfile }
return { token, user, profile, permissionCodes, isAuthenticated, sessionRestored, hasPermission, login, logout, restoreSession, acceptImpersonationCode, updateProfile, savePersonalProfile }
})