diff --git a/apps/vben5/apps/app-antd/src/layouts/basic.vue b/apps/vben5/apps/app-antd/src/layouts/basic.vue index eeb8302f4..7eb2328bf 100644 --- a/apps/vben5/apps/app-antd/src/layouts/basic.vue +++ b/apps/vben5/apps/app-antd/src/layouts/basic.vue @@ -4,7 +4,11 @@ import type { NotificationItem } from '@vben/layouts'; import { computed, defineAsyncComponent, ref, watch } from 'vue'; import { useRouter } from 'vue-router'; -import { AuthenticationLoginExpiredModal, useVbenModal } from '@vben/common-ui'; +import { + AuthenticationLoginExpiredModal, + confirm, + useVbenModal, +} from '@vben/common-ui'; import { useWatermark } from '@vben/hooks'; import { createIconifyIcon } from '@vben/icons'; import { @@ -25,6 +29,7 @@ import LoginForm from '#/views/_core/authentication/login.vue'; const UserSettingsIcon = createIconifyIcon('tdesign:user-setting'); const UserLinkIcon = createIconifyIcon('material-symbols-light:link'); +const BckLoginIcon = createIconifyIcon('lets-icons:back-light'); const notifications = ref([]); @@ -48,22 +53,32 @@ const [LinkUserModal, linkUserModalApi] = useVbenModal({ }), }); -const menus = computed(() => [ - { - handler: () => { - linkUserModalApi.open(); +const menus = computed(() => { + const basicMenus = [ + { + handler: () => { + linkUserModalApi.open(); + }, + icon: UserLinkIcon, + text: $t('abp.account.linkAccount'), }, - icon: UserLinkIcon, - text: $t('abp.account.linkAccount'), - }, - { - handler: () => { - router.replace('/account/my-settings'); + { + handler: () => { + router.replace('/account/my-settings'); + }, + icon: UserSettingsIcon, + text: $t('abp.account.settings.title'), }, - icon: UserSettingsIcon, - text: $t('abp.account.settings.title'), - }, -]); + ]; + if (abpStore.application?.currentUser.impersonatorUserId) { + basicMenus.push({ + handler: handleBackUserLogin, + icon: BckLoginIcon, + text: $t('abp.account.revertImpersonation'), + }); + } + return basicMenus; +}); const userInfo = computed(() => { return userStore.userInfo; @@ -114,6 +129,23 @@ async function handleLinkLogin(userId: string, tenantId?: string) { window.location.replace('/'); }); } + +async function handleBackUserLogin() { + confirm({ + beforeClose: async (scope) => { + if (scope.isConfirm) { + await authStore.impersonationUserLogin({}, () => { + tabbarStore.closeAllTabs(router); + window.location.replace('/'); + }); + } + return true; + }, + centered: true, + content: $t('AbpAccount.RevertImpersonationWarning'), + title: $t('AbpUi.AreYouSure'), + }); +} watch( () => preferences.app.watermark, async (enable) => { diff --git a/apps/vben5/apps/app-antd/src/locales/langs/en-US/abp.json b/apps/vben5/apps/app-antd/src/locales/langs/en-US/abp.json index bac7ca89c..80a1ecb18 100644 --- a/apps/vben5/apps/app-antd/src/locales/langs/en-US/abp.json +++ b/apps/vben5/apps/app-antd/src/locales/langs/en-US/abp.json @@ -80,6 +80,7 @@ "account": { "title": "Account", "linkAccount": "Link Account", + "revertImpersonation": "Revert My Account", "settings": { "title": "My Settings", "basic": { diff --git a/apps/vben5/apps/app-antd/src/locales/langs/zh-CN/abp.json b/apps/vben5/apps/app-antd/src/locales/langs/zh-CN/abp.json index c59c5c05e..2e0e1059c 100644 --- a/apps/vben5/apps/app-antd/src/locales/langs/zh-CN/abp.json +++ b/apps/vben5/apps/app-antd/src/locales/langs/zh-CN/abp.json @@ -80,6 +80,7 @@ "account": { "title": "账户管理", "linkAccount": "关联账户", + "revertImpersonation": "回到我的账户", "settings": { "title": "个人设置", "basic": { diff --git a/apps/vben5/apps/app-antd/src/store/auth.ts b/apps/vben5/apps/app-antd/src/store/auth.ts index 05e219d79..06791a955 100644 --- a/apps/vben5/apps/app-antd/src/store/auth.ts +++ b/apps/vben5/apps/app-antd/src/store/auth.ts @@ -142,6 +142,37 @@ export const useAuthStore = defineStore('auth', () => { } } + async function impersonationUserLogin( + params: { + tenantId?: string; + tenantUserName?: string; + userDelegationId?: string; + userId?: string; + }, + onSuccess?: () => Promise | void, + ) { + try { + loginLoading.value = true; + const accessToken = await oAuthService.getAccessToken(); + const user = await oAuthService.loginByImpersonation({ + accessToken, + ...params, + }); + return await _loginSuccess( + { + accessToken: user.access_token, + tokenType: user.token_type, + refreshToken: user.refresh_token ?? '', + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + expiresIn: user.expires_in!, + }, + onSuccess, + ); + } finally { + loginLoading.value = false; + } + } + /** * 异步处理登录操作 * Asynchronously handle the login process @@ -300,6 +331,7 @@ export const useAuthStore = defineStore('auth', () => { oidcCallback, fetchUserInfo, linkUseLogin, + impersonationUserLogin, loginLoading, logout, refreshSession, diff --git a/apps/vben5/apps/app-antd/src/views/identity/users/index.vue b/apps/vben5/apps/app-antd/src/views/identity/users/index.vue index 43c682edf..7ff94e137 100644 --- a/apps/vben5/apps/app-antd/src/views/identity/users/index.vue +++ b/apps/vben5/apps/app-antd/src/views/identity/users/index.vue @@ -1,15 +1,39 @@ diff --git a/apps/vben5/packages/@abp/account/src/hooks/useOAuthService.ts b/apps/vben5/packages/@abp/account/src/hooks/useOAuthService.ts index d5e87dd90..63502c09c 100644 --- a/apps/vben5/packages/@abp/account/src/hooks/useOAuthService.ts +++ b/apps/vben5/packages/@abp/account/src/hooks/useOAuthService.ts @@ -1,6 +1,7 @@ import type { SigninRedirectArgs, SignoutRedirectArgs } from 'oidc-client-ts'; import type { + ImpersonationTokenRequest, LinkUserTokenRequest, PasswordTokenRequestModel, PhoneNumberTokenRequest, @@ -18,6 +19,10 @@ export function useOAuthService() { return userManager.signinLinkUser(input); } + async function loginByImpersonation(input: ImpersonationTokenRequest) { + return userManager.signinImpersonation(input); + } + async function loginByPassword(input: PasswordTokenRequestModel) { return userManager.signinResourceOwnerCredentials(input); } @@ -67,6 +72,7 @@ export function useOAuthService() { loginBySmsCode, loginByQrCode, logout, + loginByImpersonation, refreshToken, revokeTokens, getAccessToken, diff --git a/apps/vben5/packages/@abp/account/src/types/token.ts b/apps/vben5/packages/@abp/account/src/types/token.ts index cf06a2e11..f1513c0b9 100644 --- a/apps/vben5/packages/@abp/account/src/types/token.ts +++ b/apps/vben5/packages/@abp/account/src/types/token.ts @@ -47,6 +47,19 @@ interface LinkUserTokenRequest { /** 关联用户Id */ linkUserId: string; } +/** 模拟用户授权请求数据模型 */ +interface ImpersonationTokenRequest { + /** 当前用户访问令牌 */ + accessToken?: string; + /** 模拟租户Id */ + tenantId?: string; + /** 模拟租户用户名 */ + tenantUserName?: string; + /** 委托用户Id */ + userDelegationId?: string; + /** 模拟用户Id */ + userId?: string; +} /** 令牌撤销请求数据类型 */ interface RevokeTokenRequest { /** 令牌 */ @@ -101,6 +114,7 @@ interface ShouldChangePasswordError extends OAuthError { } export type { + ImpersonationTokenRequest, LinkUserTokenRequest, OAuthError, OAuthTokenRefreshModel, diff --git a/apps/vben5/packages/@abp/account/src/utils/auth.ts b/apps/vben5/packages/@abp/account/src/utils/auth.ts index b32ca9ae9..9e3c78ec7 100644 --- a/apps/vben5/packages/@abp/account/src/utils/auth.ts +++ b/apps/vben5/packages/@abp/account/src/utils/auth.ts @@ -1,6 +1,7 @@ import type { Logger, UserManagerSettings } from 'oidc-client-ts'; import type { + ImpersonationTokenRequest, LinkUserTokenRequest, PasswordTokenRequestModel, PhoneNumberTokenRequest, @@ -72,6 +73,31 @@ class AbpUserManager extends UserManager { params.set('userId', model.userId); } } + async signinImpersonation(params: ImpersonationTokenRequest) { + const logger = this._logger.create('signinImpersonationUser'); + const body = new URLSearchParams({ + grant_type: 'impersonation', + client_id: this.settings.client_id, + }); + if (params.accessToken) { + body.set('access_token', params.accessToken); + } + if (params.userId) { + body.set('UserId', params.userId); + } + if (params.userDelegationId) { + body.set('UserDelegationId', params.userDelegationId); + } + if (params.tenantUserName) { + body.set('TenantUserName', params.tenantUserName); + } + const client_secret = this.settings.client_secret; + if (client_secret) { + body.set('client_secret', client_secret); + } + this._writeTwoFactorToken(body, params); + return await this._fetchUser(logger, body); + } async signinLinkUser(params: LinkUserTokenRequest) { const logger = this._logger.create('signinLinkUser'); const body = new URLSearchParams({ @@ -89,7 +115,6 @@ class AbpUserManager extends UserManager { if (client_secret) { body.set('client_secret', client_secret); } - this._writeUserId(body, params); this._writeTenantId(body, params); this._writeTwoFactorToken(body, params); return await this._fetchUser(logger, body); diff --git a/apps/vben5/packages/@abp/identity/src/components/users/UserTable.vue b/apps/vben5/packages/@abp/identity/src/components/users/UserTable.vue index 520e3d659..ae80c72c6 100644 --- a/apps/vben5/packages/@abp/identity/src/components/users/UserTable.vue +++ b/apps/vben5/packages/@abp/identity/src/components/users/UserTable.vue @@ -23,6 +23,7 @@ import { EditOutlined, EllipsisOutlined, LockOutlined, + LoginOutlined, PlusOutlined, UnlockOutlined, } from '@ant-design/icons-vue'; @@ -38,6 +39,10 @@ defineOptions({ name: 'UserTable', }); +const emit = defineEmits<{ + (event: 'impersonation', user: IdentityUserDto): void; +}>(); + const UserModal = defineAsyncComponent(() => import('./UserModal.vue')); const LockModal = defineAsyncComponent(() => import('./UserLockModal.vue')); const ClaimModal = defineAsyncComponent(() => import('./UserClaimModal.vue')); @@ -46,6 +51,7 @@ const PasswordModal = defineAsyncComponent( ); const MenuItem = Menu.Item; +const MenuDivider = Menu.Divider; const CheckIcon = createIconifyIcon('ant-design:check-outlined'); const CloseIcon = createIconifyIcon('ant-design:close-outlined'); const PasswordIcon = createIconifyIcon('carbon:password'); @@ -239,6 +245,17 @@ const handleDelete = (row: IdentityUserDto) => { }); }; +const handleImpersonationUser = (row: IdentityUserDto) => { + Modal.confirm({ + centered: true, + content: $t('AbpAccount.ImpersonatorWarning', [row.userName]), + onOk: () => { + emit('impersonation', row); + }, + title: $t('AbpUi.AreYouSure'), + }); +}; + const handleUnlock = async (row: IdentityUserDto) => { await unLockApi(row.id); await gridApi.query(); @@ -260,6 +277,10 @@ const handleMenuClick = async (row: IdentityUserDto, info: MenuInfo) => { userChangeDrawerApi.open(); break; } + case 'impersonation': { + handleImpersonationUser(row); + break; + } case 'lock': { lockModalApi.setData(row); lockModalApi.open(); @@ -373,7 +394,7 @@ const handleMenuClick = async (row: IdentityUserDto, info: MenuInfo) => {