From c4540c1c0c51831968808c1459470a452cd5f22d Mon Sep 17 00:00:00 2001
From: ouyangxq <775153912@qq.com>
Date: Sat, 8 Aug 2026 10:14:07 +0800
Subject: [PATCH] =?UTF-8?q?feat:=20=E6=94=AF=E6=8C=81=E5=B9=B3=E5=8F=B0?=
=?UTF-8?q?=E4=BB=A3=E7=99=BB=E5=BD=95=E5=AE=A2=E6=88=B7=E5=B7=A5=E4=BD=9C?=
=?UTF-8?q?=E5=8F=B0?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.env.example | 1 +
Dockerfile | 2 ++
docker-compose.test.yml | 2 ++
src/App.vue | 35 ++++++++++++++++++++++++++++++---
src/api/session.ts | 5 +++++
src/modules/auth/api/auth.ts | 10 ++++++++++
src/modules/auth/store/auth.ts | 36 +++++++++++++++++++++++++++++++---
7 files changed, 85 insertions(+), 6 deletions(-)
diff --git a/.env.example b/.env.example
index b43cf89..64e1af2 100644
--- a/.env.example
+++ b/.env.example
@@ -1,2 +1,3 @@
VITE_API_BASE_URL=/api
API_PROXY_TARGET=http://8.163.30.128:9000
+VITE_ADMIN_WEB_ORIGIN=http://localhost:5173
diff --git a/Dockerfile b/Dockerfile
index 4d48142..a747a6c 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -16,6 +16,8 @@ RUN pnpm config set registry https://registry.npmmirror.com && \
pnpm install --frozen-lockfile
COPY . .
+ARG VITE_ADMIN_WEB_ORIGIN
+ENV VITE_ADMIN_WEB_ORIGIN=${VITE_ADMIN_WEB_ORIGIN}
RUN pnpm build
# 运行阶段仅保留构建产物与 Nginx。
diff --git a/docker-compose.test.yml b/docker-compose.test.yml
index b463d8b..3404d87 100644
--- a/docker-compose.test.yml
+++ b/docker-compose.test.yml
@@ -3,6 +3,8 @@ services:
build:
context: .
dockerfile: Dockerfile
+ args:
+ VITE_ADMIN_WEB_ORIGIN: "${VITE_ADMIN_WEB_ORIGIN:-http://8.163.30.128:9001}"
image: client-web-test:local
container_name: client-web-test
restart: unless-stopped
diff --git a/src/App.vue b/src/App.vue
index 4be8965..f38f8d2 100644
--- a/src/App.vue
+++ b/src/App.vue
@@ -1,4 +1,33 @@
-
-
-
+
+
+
diff --git a/src/api/session.ts b/src/api/session.ts
index c3d38f6..7c01340 100644
--- a/src/api/session.ts
+++ b/src/api/session.ts
@@ -29,6 +29,11 @@ export function persistTokenPair(tokens: TokenPair, remember?: boolean) {
storage.setItem(REFRESH_TOKEN_KEY, tokens.refreshToken)
}
+export function persistTemporaryAccessToken(accessToken: string) {
+ clearSessionTokens()
+ sessionStorage.setItem(ACCESS_TOKEN_KEY, accessToken)
+}
+
export function clearSessionTokens() {
for (const storage of [localStorage, sessionStorage]) {
storage.removeItem(ACCESS_TOKEN_KEY)
diff --git a/src/modules/auth/api/auth.ts b/src/modules/auth/api/auth.ts
index b310ef0..f0fc43e 100644
--- a/src/modules/auth/api/auth.ts
+++ b/src/modules/auth/api/auth.ts
@@ -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({ 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({ method: 'PUT', url: '/auth/password', data: { oldPassword, newPassword } })
}
+
+export function exchangeImpersonationCode(code: string) {
+ return apiRequest({ method: 'POST', url: '/auth/impersonations/exchange', data: { code } })
+}
diff --git a/src/modules/auth/store/auth.ts b/src/modules/auth/store/auth.ts
index 51760be..5ce4c28 100644
--- a/src/modules/auth/store/auth.ts
+++ b/src/modules/auth/store/auth.ts
@@ -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) => {
+ 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 }
})