Browse Source

feat: Implement the user impersonation login ui

pull/1525/head
colin 1 month 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 { 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<NotificationItem[]>([]);
@ -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) => {

1
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": {

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

@ -80,6 +80,7 @@
"account": {
"title": "账户管理",
"linkAccount": "关联账户",
"revertImpersonation": "回到我的账户",
"settings": {
"title": "个人设置",
"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
@ -300,6 +331,7 @@ export const useAuthStore = defineStore('auth', () => {
oidcCallback,
fetchUserInfo,
linkUseLogin,
impersonationUserLogin,
loginLoading,
logout,
refreshSession,

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

@ -1,15 +1,39 @@
<script lang="ts" setup>
import type { IdentityUserDto } from '@abp/identity';
import { useRouter } from 'vue-router';
import { Page } from '@vben/common-ui';
import { useTabbarStore } from '@vben/stores';
import { UserTable } from '@abp/identity';
import { useAuthStore } from '#/store/auth';
defineOptions({
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>
<template>
<Page>
<UserTable />
<UserTable @impersonation="handleImpersonation" />
</Page>
</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 {
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,

14
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,

27
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);

36
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) => {
<div class="basis-1/3">
<Dropdown>
<template #overlay>
<Menu @click="(info) => handleMenuClick(row, info)">
<Menu @click="(info: MenuInfo) => handleMenuClick(row, info)">
<MenuItem
v-if="
hasAccessByCodes([IdentityUserPermissions.Update]) &&
@ -396,6 +417,7 @@ const handleMenuClick = async (row: IdentityUserDto, info: MenuInfo) => {
>
{{ $t('AbpIdentity.UnLock') }}
</MenuItem>
<MenuDivider />
<MenuItem
v-if="
hasAccessByCodes([
@ -423,6 +445,16 @@ const handleMenuClick = async (row: IdentityUserDto, info: MenuInfo) => {
>
{{ $t('AbpIdentity.ManageClaim') }}
</MenuItem>
<MenuItem
v-if="
hasAccessByCodes([IdentityUserPermissions.Impersonation])
"
key="impersonation"
:icon="h(LoginOutlined)"
>
{{ $t('AbpIdentity.ImpersonationUser') }}
</MenuItem>
<MenuDivider />
<MenuItem
v-if="hasAccessByCodes([IdentityUserPermissions.Update])"
key="password"
@ -430,6 +462,7 @@ const handleMenuClick = async (row: IdentityUserDto, info: MenuInfo) => {
>
{{ $t('AbpIdentity.SetPassword') }}
</MenuItem>
<MenuDivider />
<MenuItem
v-if="hasAccessByCodes(['Platform.Menu.ManageUsers'])"
key="menus"
@ -437,6 +470,7 @@ const handleMenuClick = async (row: IdentityUserDto, info: MenuInfo) => {
>
{{ $t('AppPlatform.Menu:Manage') }}
</MenuItem>
<MenuDivider />
<MenuItem
v-if="
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',
/** 更新 */
Update: 'AbpIdentity.Users.Update',
/** 模拟用户 */
Impersonation: 'AbpIdentity.Users.Impersonation',
};
/** 角色权限 */
export const IdentityRolePermissions = {

Loading…
Cancel
Save