Browse Source

feat: Implement the user impersonation login ui

pull/1525/head
colin 3 months ago
parent
commit
c5394bb8e4
  1. 62
      apps/vben5/apps/app-antd/src/layouts/basic.vue
  2. 1
      apps/vben5/apps/app-antd/src/locales/langs/en-US/abp.json
  3. 1
      apps/vben5/apps/app-antd/src/locales/langs/zh-CN/abp.json
  4. 32
      apps/vben5/apps/app-antd/src/store/auth.ts
  5. 26
      apps/vben5/apps/app-antd/src/views/identity/users/index.vue
  6. 6
      apps/vben5/packages/@abp/account/src/hooks/useOAuthService.ts
  7. 14
      apps/vben5/packages/@abp/account/src/types/token.ts
  8. 27
      apps/vben5/packages/@abp/account/src/utils/auth.ts
  9. 36
      apps/vben5/packages/@abp/identity/src/components/users/UserTable.vue
  10. 2
      apps/vben5/packages/@abp/identity/src/constants/permissions.ts

62
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 { computed, defineAsyncComponent, ref, watch } from 'vue';
import { useRouter } from 'vue-router'; 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 { useWatermark } from '@vben/hooks';
import { createIconifyIcon } from '@vben/icons'; import { createIconifyIcon } from '@vben/icons';
import { import {
@ -25,6 +29,7 @@ import LoginForm from '#/views/_core/authentication/login.vue';
const UserSettingsIcon = createIconifyIcon('tdesign:user-setting'); const UserSettingsIcon = createIconifyIcon('tdesign:user-setting');
const UserLinkIcon = createIconifyIcon('material-symbols-light:link'); const UserLinkIcon = createIconifyIcon('material-symbols-light:link');
const BckLoginIcon = createIconifyIcon('lets-icons:back-light');
const notifications = ref<NotificationItem[]>([]); const notifications = ref<NotificationItem[]>([]);
@ -48,22 +53,32 @@ const [LinkUserModal, linkUserModalApi] = useVbenModal({
}), }),
}); });
const menus = computed(() => [ const menus = computed(() => {
{ const basicMenus = [
handler: () => { {
linkUserModalApi.open(); handler: () => {
linkUserModalApi.open();
},
icon: UserLinkIcon,
text: $t('abp.account.linkAccount'),
}, },
icon: UserLinkIcon, {
text: $t('abp.account.linkAccount'), handler: () => {
}, router.replace('/account/my-settings');
{ },
handler: () => { icon: UserSettingsIcon,
router.replace('/account/my-settings'); 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(() => { const userInfo = computed(() => {
return userStore.userInfo; return userStore.userInfo;
@ -114,6 +129,23 @@ async function handleLinkLogin(userId: string, tenantId?: string) {
window.location.replace('/'); 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( watch(
() => preferences.app.watermark, () => preferences.app.watermark,
async (enable) => { async (enable) => {

1
apps/vben5/apps/app-antd/src/locales/langs/en-US/abp.json

@ -80,6 +80,7 @@
"account": { "account": {
"title": "Account", "title": "Account",
"linkAccount": "Link Account", "linkAccount": "Link Account",
"revertImpersonation": "Revert My Account",
"settings": { "settings": {
"title": "My Settings", "title": "My Settings",
"basic": { "basic": {

1
apps/vben5/apps/app-antd/src/locales/langs/zh-CN/abp.json

@ -80,6 +80,7 @@
"account": { "account": {
"title": "账户管理", "title": "账户管理",
"linkAccount": "关联账户", "linkAccount": "关联账户",
"revertImpersonation": "回到我的账户",
"settings": { "settings": {
"title": "个人设置", "title": "个人设置",
"basic": { "basic": {

32
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> | 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 * Asynchronously handle the login process
@ -300,6 +331,7 @@ export const useAuthStore = defineStore('auth', () => {
oidcCallback, oidcCallback,
fetchUserInfo, fetchUserInfo,
linkUseLogin, linkUseLogin,
impersonationUserLogin,
loginLoading, loginLoading,
logout, logout,
refreshSession, refreshSession,

26
apps/vben5/apps/app-antd/src/views/identity/users/index.vue

@ -1,15 +1,39 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { IdentityUserDto } from '@abp/identity';
import { useRouter } from 'vue-router';
import { Page } from '@vben/common-ui'; import { Page } from '@vben/common-ui';
import { useTabbarStore } from '@vben/stores';
import { UserTable } from '@abp/identity'; import { UserTable } from '@abp/identity';
import { useAuthStore } from '#/store/auth';
defineOptions({ defineOptions({
name: 'Vben5IdentityUsers', name: 'Vben5IdentityUsers',
}); });
const router = useRouter();
const authStore = useAuthStore();
const tabbarStore = useTabbarStore();
async function handleImpersonation(user: IdentityUserDto) {
await authStore.impersonationUserLogin(
{
userId: user.id,
tenantId: user.tenentId,
},
() => {
tabbarStore.closeAllTabs(router);
window.location.replace('/');
},
);
}
</script> </script>
<template> <template>
<Page> <Page>
<UserTable /> <UserTable @impersonation="handleImpersonation" />
</Page> </Page>
</template> </template>

6
apps/vben5/packages/@abp/account/src/hooks/useOAuthService.ts

@ -1,6 +1,7 @@
import type { SigninRedirectArgs, SignoutRedirectArgs } from 'oidc-client-ts'; import type { SigninRedirectArgs, SignoutRedirectArgs } from 'oidc-client-ts';
import type { import type {
ImpersonationTokenRequest,
LinkUserTokenRequest, LinkUserTokenRequest,
PasswordTokenRequestModel, PasswordTokenRequestModel,
PhoneNumberTokenRequest, PhoneNumberTokenRequest,
@ -18,6 +19,10 @@ export function useOAuthService() {
return userManager.signinLinkUser(input); return userManager.signinLinkUser(input);
} }
async function loginByImpersonation(input: ImpersonationTokenRequest) {
return userManager.signinImpersonation(input);
}
async function loginByPassword(input: PasswordTokenRequestModel) { async function loginByPassword(input: PasswordTokenRequestModel) {
return userManager.signinResourceOwnerCredentials(input); return userManager.signinResourceOwnerCredentials(input);
} }
@ -67,6 +72,7 @@ export function useOAuthService() {
loginBySmsCode, loginBySmsCode,
loginByQrCode, loginByQrCode,
logout, logout,
loginByImpersonation,
refreshToken, refreshToken,
revokeTokens, revokeTokens,
getAccessToken, getAccessToken,

14
apps/vben5/packages/@abp/account/src/types/token.ts

@ -47,6 +47,19 @@ interface LinkUserTokenRequest {
/** 关联用户Id */ /** 关联用户Id */
linkUserId: string; linkUserId: string;
} }
/** 模拟用户授权请求数据模型 */
interface ImpersonationTokenRequest {
/** 当前用户访问令牌 */
accessToken?: string;
/** 模拟租户Id */
tenantId?: string;
/** 模拟租户用户名 */
tenantUserName?: string;
/** 委托用户Id */
userDelegationId?: string;
/** 模拟用户Id */
userId?: string;
}
/** 令牌撤销请求数据类型 */ /** 令牌撤销请求数据类型 */
interface RevokeTokenRequest { interface RevokeTokenRequest {
/** 令牌 */ /** 令牌 */
@ -101,6 +114,7 @@ interface ShouldChangePasswordError extends OAuthError {
} }
export type { export type {
ImpersonationTokenRequest,
LinkUserTokenRequest, LinkUserTokenRequest,
OAuthError, OAuthError,
OAuthTokenRefreshModel, OAuthTokenRefreshModel,

27
apps/vben5/packages/@abp/account/src/utils/auth.ts

@ -1,6 +1,7 @@
import type { Logger, UserManagerSettings } from 'oidc-client-ts'; import type { Logger, UserManagerSettings } from 'oidc-client-ts';
import type { import type {
ImpersonationTokenRequest,
LinkUserTokenRequest, LinkUserTokenRequest,
PasswordTokenRequestModel, PasswordTokenRequestModel,
PhoneNumberTokenRequest, PhoneNumberTokenRequest,
@ -72,6 +73,31 @@ class AbpUserManager extends UserManager {
params.set('userId', model.userId); 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) { async signinLinkUser(params: LinkUserTokenRequest) {
const logger = this._logger.create('signinLinkUser'); const logger = this._logger.create('signinLinkUser');
const body = new URLSearchParams({ const body = new URLSearchParams({
@ -89,7 +115,6 @@ class AbpUserManager extends UserManager {
if (client_secret) { if (client_secret) {
body.set('client_secret', client_secret); body.set('client_secret', client_secret);
} }
this._writeUserId(body, params);
this._writeTenantId(body, params); this._writeTenantId(body, params);
this._writeTwoFactorToken(body, params); this._writeTwoFactorToken(body, params);
return await this._fetchUser(logger, body); return await this._fetchUser(logger, body);

36
apps/vben5/packages/@abp/identity/src/components/users/UserTable.vue

@ -23,6 +23,7 @@ import {
EditOutlined, EditOutlined,
EllipsisOutlined, EllipsisOutlined,
LockOutlined, LockOutlined,
LoginOutlined,
PlusOutlined, PlusOutlined,
UnlockOutlined, UnlockOutlined,
} from '@ant-design/icons-vue'; } from '@ant-design/icons-vue';
@ -38,6 +39,10 @@ defineOptions({
name: 'UserTable', name: 'UserTable',
}); });
const emit = defineEmits<{
(event: 'impersonation', user: IdentityUserDto): void;
}>();
const UserModal = defineAsyncComponent(() => import('./UserModal.vue')); const UserModal = defineAsyncComponent(() => import('./UserModal.vue'));
const LockModal = defineAsyncComponent(() => import('./UserLockModal.vue')); const LockModal = defineAsyncComponent(() => import('./UserLockModal.vue'));
const ClaimModal = defineAsyncComponent(() => import('./UserClaimModal.vue')); const ClaimModal = defineAsyncComponent(() => import('./UserClaimModal.vue'));
@ -46,6 +51,7 @@ const PasswordModal = defineAsyncComponent(
); );
const MenuItem = Menu.Item; const MenuItem = Menu.Item;
const MenuDivider = Menu.Divider;
const CheckIcon = createIconifyIcon('ant-design:check-outlined'); const CheckIcon = createIconifyIcon('ant-design:check-outlined');
const CloseIcon = createIconifyIcon('ant-design:close-outlined'); const CloseIcon = createIconifyIcon('ant-design:close-outlined');
const PasswordIcon = createIconifyIcon('carbon:password'); 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) => { const handleUnlock = async (row: IdentityUserDto) => {
await unLockApi(row.id); await unLockApi(row.id);
await gridApi.query(); await gridApi.query();
@ -260,6 +277,10 @@ const handleMenuClick = async (row: IdentityUserDto, info: MenuInfo) => {
userChangeDrawerApi.open(); userChangeDrawerApi.open();
break; break;
} }
case 'impersonation': {
handleImpersonationUser(row);
break;
}
case 'lock': { case 'lock': {
lockModalApi.setData(row); lockModalApi.setData(row);
lockModalApi.open(); lockModalApi.open();
@ -373,7 +394,7 @@ const handleMenuClick = async (row: IdentityUserDto, info: MenuInfo) => {
<div class="basis-1/3"> <div class="basis-1/3">
<Dropdown> <Dropdown>
<template #overlay> <template #overlay>
<Menu @click="(info) => handleMenuClick(row, info)"> <Menu @click="(info: MenuInfo) => handleMenuClick(row, info)">
<MenuItem <MenuItem
v-if=" v-if="
hasAccessByCodes([IdentityUserPermissions.Update]) && hasAccessByCodes([IdentityUserPermissions.Update]) &&
@ -396,6 +417,7 @@ const handleMenuClick = async (row: IdentityUserDto, info: MenuInfo) => {
> >
{{ $t('AbpIdentity.UnLock') }} {{ $t('AbpIdentity.UnLock') }}
</MenuItem> </MenuItem>
<MenuDivider />
<MenuItem <MenuItem
v-if=" v-if="
hasAccessByCodes([ hasAccessByCodes([
@ -423,6 +445,16 @@ const handleMenuClick = async (row: IdentityUserDto, info: MenuInfo) => {
> >
{{ $t('AbpIdentity.ManageClaim') }} {{ $t('AbpIdentity.ManageClaim') }}
</MenuItem> </MenuItem>
<MenuItem
v-if="
hasAccessByCodes([IdentityUserPermissions.Impersonation])
"
key="impersonation"
:icon="h(LoginOutlined)"
>
{{ $t('AbpIdentity.ImpersonationUser') }}
</MenuItem>
<MenuDivider />
<MenuItem <MenuItem
v-if="hasAccessByCodes([IdentityUserPermissions.Update])" v-if="hasAccessByCodes([IdentityUserPermissions.Update])"
key="password" key="password"
@ -430,6 +462,7 @@ const handleMenuClick = async (row: IdentityUserDto, info: MenuInfo) => {
> >
{{ $t('AbpIdentity.SetPassword') }} {{ $t('AbpIdentity.SetPassword') }}
</MenuItem> </MenuItem>
<MenuDivider />
<MenuItem <MenuItem
v-if="hasAccessByCodes(['Platform.Menu.ManageUsers'])" v-if="hasAccessByCodes(['Platform.Menu.ManageUsers'])"
key="menus" key="menus"
@ -437,6 +470,7 @@ const handleMenuClick = async (row: IdentityUserDto, info: MenuInfo) => {
> >
{{ $t('AppPlatform.Menu:Manage') }} {{ $t('AppPlatform.Menu:Manage') }}
</MenuItem> </MenuItem>
<MenuDivider />
<MenuItem <MenuItem
v-if=" v-if="
isEnabled('AbpAuditing.Logging.AuditLog') && isEnabled('AbpAuditing.Logging.AuditLog') &&

2
apps/vben5/packages/@abp/identity/src/constants/permissions.ts

@ -10,6 +10,8 @@ export const IdentityUserPermissions = {
ManagePermissions: 'AbpIdentity.Users.ManagePermissions', ManagePermissions: 'AbpIdentity.Users.ManagePermissions',
/** 更新 */ /** 更新 */
Update: 'AbpIdentity.Users.Update', Update: 'AbpIdentity.Users.Update',
/** 模拟用户 */
Impersonation: 'AbpIdentity.Users.Impersonation',
}; };
/** 角色权限 */ /** 角色权限 */
export const IdentityRolePermissions = { export const IdentityRolePermissions = {

Loading…
Cancel
Save