Browse Source

🚧 feat(session): 增加用户会话管理.

pull/1087/head
colin 1 year ago
parent
commit
e2eea3b7ca
  1. 3
      apps/vben5/apps/app-antd/src/locales/langs/en-US/abp.json
  2. 3
      apps/vben5/apps/app-antd/src/locales/langs/zh-CN/abp.json
  3. 1
      apps/vben5/packages/@abp/account/package.json
  4. 1
      apps/vben5/packages/@abp/account/src/api/index.ts
  5. 41
      apps/vben5/packages/@abp/account/src/api/useMySessionApi.ts
  6. 28
      apps/vben5/packages/@abp/account/src/components/MySetting.vue
  7. 48
      apps/vben5/packages/@abp/account/src/components/components/SessionSettings.vue
  8. 1
      apps/vben5/packages/@abp/identity/src/api/index.ts
  9. 42
      apps/vben5/packages/@abp/identity/src/api/useUserSessionsApi.ts
  10. 1
      apps/vben5/packages/@abp/identity/src/components/index.ts
  11. 158
      apps/vben5/packages/@abp/identity/src/components/sessions/SessionTable.vue
  12. 67
      apps/vben5/packages/@abp/identity/src/components/users/UserSessionDrawer.vue
  13. 24
      apps/vben5/packages/@abp/identity/src/components/users/UserTable.vue
  14. 6
      apps/vben5/packages/@abp/identity/src/constants/permissions.ts
  15. 1
      apps/vben5/packages/@abp/identity/src/types/index.ts
  16. 25
      apps/vben5/packages/@abp/identity/src/types/sessions.ts

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

@ -59,7 +59,8 @@
"bindSettings": "Bind Settings",
"noticeSettings": "Notice Settings",
"authenticatorSettings": "Authenticator Settings",
"changeAvatar": "Change Avatar"
"changeAvatar": "Change Avatar",
"sessionSettings": "Session Settings"
},
"profile": "My Profile"
}

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

@ -59,7 +59,8 @@
"bindSettings": "账号绑定",
"noticeSettings": "新消息通知",
"authenticatorSettings": "身份验证程序",
"changeAvatar": "更改头像"
"changeAvatar": "更改头像",
"sessionSettings": "会话管理"
},
"profile": "个人中心"
}

1
apps/vben5/packages/@abp/account/package.json

@ -21,6 +21,7 @@
},
"dependencies": {
"@abp/core": "workspace:*",
"@abp/identity": "workspace:*",
"@abp/request": "workspace:*",
"@abp/ui": "workspace:*",
"@ant-design/icons-vue": "catalog:",

1
apps/vben5/packages/@abp/account/src/api/index.ts

@ -1,4 +1,5 @@
export { useAccountApi } from './useAccountApi';
export { useMySessionApi } from './useMySessionApi';
export { useProfileApi } from './useProfileApi';
export { useTokenApi } from './useTokenApi';
export { useUserInfoApi } from './useUserInfoApi';

41
apps/vben5/packages/@abp/account/src/api/useMySessionApi.ts

@ -0,0 +1,41 @@
import type { PagedResultDto } from '@abp/core';
import type { GetMySessionsInput, IdentitySessionDto } from '@abp/identity';
import { useRequest } from '@abp/request';
export function useMySessionApi() {
const { cancel, request } = useRequest();
/**
*
* @param { GetMySessionsInput } input
* @returns { Promise<PagedResultDto<IdentitySessionDto>> }
*/
function getSessionsApi(
input?: GetMySessionsInput,
): Promise<PagedResultDto<IdentitySessionDto>> {
return request<PagedResultDto<IdentitySessionDto>>(
'/api/account/my-profile/sessions',
{
method: 'GET',
params: input,
},
);
}
/**
*
* @param { string } sessionId id
* @returns { Promise<void> }
*/
function revokeSessionApi(sessionId: string): Promise<void> {
return request(`/api/account/my-profile/sessions/${sessionId}/revoke`, {
method: 'DELETE',
});
}
return {
cancel,
getSessionsApi,
revokeSessionApi,
};
}

28
apps/vben5/packages/@abp/account/src/components/MySetting.vue

@ -12,12 +12,25 @@ import { useUserStore } from '@vben/stores';
import { Card, Menu, message, Modal } from 'ant-design-vue';
import { useProfileApi } from '../api/useProfileApi';
import AuthenticatorSettings from './components/AuthenticatorSettings.vue';
import BasicSettings from './components/BasicSettings.vue';
import BindSettings from './components/BindSettings.vue';
import NoticeSettings from './components/NoticeSettings.vue';
import SecuritySettings from './components/SecuritySettings.vue';
const AuthenticatorSettings = defineAsyncComponent(
() => import('./components/AuthenticatorSettings.vue'),
);
const BasicSettings = defineAsyncComponent(
() => import('./components/BasicSettings.vue'),
);
const BindSettings = defineAsyncComponent(
() => import('./components/BindSettings.vue'),
);
const NoticeSettings = defineAsyncComponent(
() => import('./components/NoticeSettings.vue'),
);
const SecuritySettings = defineAsyncComponent(
() => import('./components/SecuritySettings.vue'),
);
const SessionSettings = defineAsyncComponent(
() => import('./components/SessionSettings.vue'),
);
const { getApi, updateApi } = useProfileApi();
const userStore = useUserStore();
const { query } = useRoute();
@ -37,6 +50,10 @@ const menuItems = reactive([
key: 'bind',
label: $t('abp.account.settings.bindSettings'),
},
{
key: 'session',
label: $t('abp.account.settings.sessionSettings'),
},
{
key: 'notice',
label: $t('abp.account.settings.noticeSettings'),
@ -137,6 +154,7 @@ onMounted(async () => {
<AuthenticatorSettings
v-else-if="selectedMenuKeys[0] === 'authenticator'"
/>
<SessionSettings v-else-if="selectedMenuKeys[0] === 'session'" />
</div>
</div>
</Card>

48
apps/vben5/packages/@abp/account/src/components/components/SessionSettings.vue

@ -0,0 +1,48 @@
<script setup lang="ts">
import type { IdentitySessionDto } from '@abp/identity';
import { onMounted, ref } from 'vue';
import { $t } from '@vben/locales';
import { SessionTable } from '@abp/identity';
import { Card, message, Modal } from 'ant-design-vue';
import { useMySessionApi } from '../../api/useMySessionApi';
const { cancel, getSessionsApi, revokeSessionApi } = useMySessionApi();
const sessions = ref<IdentitySessionDto[]>([]);
async function getSessions() {
const { items } = await getSessionsApi();
sessions.value = items;
}
async function onRevoke(session: IdentitySessionDto) {
Modal.confirm({
centered: true,
content: $t('AbpIdentity.SessionWillBeRevokedMessage'),
iconType: 'warning',
onCancel: () => {
cancel();
},
onOk: async () => {
await revokeSessionApi(session.sessionId);
message.success($t('AbpIdentity.SuccessfullyRevoked'));
await getSessions();
},
title: $t('AbpUi.AreYouSure'),
});
}
onMounted(getSessions);
</script>
<template>
<Card :bordered="false" :title="$t('abp.account.settings.sessionSettings')">
<SessionTable :sessions="sessions" @revoke="onRevoke" />
</Card>
</template>
<style scoped></style>

1
apps/vben5/packages/@abp/identity/src/api/index.ts

@ -4,3 +4,4 @@ export { useRolesApi } from './useRolesApi';
export { useSecurityLogsApi } from './useSecurityLogsApi';
export { useUserLookupApi } from './useUserLookupApi';
export { useUsersApi } from './useUsersApi';
export { useUserSessionsApi } from './useUserSessionsApi';

42
apps/vben5/packages/@abp/identity/src/api/useUserSessionsApi.ts

@ -0,0 +1,42 @@
import type { PagedResultDto } from '@abp/core';
import type { GetUserSessionsInput, IdentitySessionDto } from '../types';
import { useRequest } from '@abp/request';
export function useUserSessionsApi() {
const { cancel, request } = useRequest();
/**
*
* @param { GetUserSessionsInput } input
* @returns { Promise<PagedResultDto<IdentitySessionDto>> }
*/
function getSessionsApi(
input?: GetUserSessionsInput,
): Promise<PagedResultDto<IdentitySessionDto>> {
return request<PagedResultDto<IdentitySessionDto>>(
'/api/identity/sessions',
{
method: 'GET',
params: input,
},
);
}
/**
*
* @param { string } sessionId id
* @returns { Promise<void> }
*/
function revokeSessionApi(sessionId: string): Promise<void> {
return request(`/api/identity/sessions/${sessionId}/revoke`, {
method: 'DELETE',
});
}
return {
cancel,
getSessionsApi,
revokeSessionApi,
};
}

1
apps/vben5/packages/@abp/identity/src/components/index.ts

@ -2,4 +2,5 @@ export { default as ClaimTypeTable } from './claim-types/ClaimTypeTable.vue';
export { default as OrganizationUnitPage } from './organization-units/OrganizationUnitPage.vue';
export { default as RoleTable } from './roles/RoleTable.vue';
export { default as SecurityLogTable } from './security-logs/SecurityLogTable.vue';
export { default as SessionTable } from './sessions/SessionTable.vue';
export { default as UserTable } from './users/UserTable.vue';

158
apps/vben5/packages/@abp/identity/src/components/sessions/SessionTable.vue

@ -0,0 +1,158 @@
<script setup lang="ts">
import type { VxeGridProps } from '@abp/ui';
import type { IdentitySessionDto } from '../../types/sessions';
import { computed, nextTick, reactive, watch } from 'vue';
import { useAccess } from '@vben/access';
import { $t } from '@vben/locales';
import { useAbpStore } from '@abp/core';
import { useVbenVxeGrid } from '@abp/ui';
import { Button, Descriptions, Tag } from 'ant-design-vue';
import { IdentitySessionPermissions } from '../../constants/permissions';
const props = defineProps<{
sessions: IdentitySessionDto[];
}>();
const emits = defineEmits<{
(event: 'revoke', session: IdentitySessionDto): void;
}>();
const DescriptionItem = Descriptions.Item;
const { hasAccessByCodes } = useAccess();
const abpStore = useAbpStore();
/** 获取登录用户会话Id */
const getMySessionId = computed(() => {
return abpStore.application?.currentUser.sessionId;
});
/** 获取是否允许撤销会话 */
const getAllowRevokeSession = computed(() => {
return (session: IdentitySessionDto) => {
if (getMySessionId.value === session.sessionId) {
return false;
}
return hasAccessByCodes([IdentitySessionPermissions.Revoke]);
};
});
const gridOptions = reactive<VxeGridProps<IdentitySessionDto>>({
columns: [
{
align: 'left',
slots: { content: 'deviceInfo' },
type: 'expand',
width: 50,
},
{
align: 'left',
field: 'device',
slots: { default: 'device' },
title: $t('AbpIdentity.DisplayName:Device'),
width: 150,
},
{
align: 'left',
field: 'signedIn',
minWidth: 200,
title: $t('AbpIdentity.DisplayName:SignedIn'),
},
{
field: 'action',
fixed: 'right',
slots: { default: 'action' },
title: $t('AbpUi.Actions'),
width: 120,
},
],
expandConfig: {
padding: true,
trigger: 'default',
},
exportConfig: {},
keepSource: true,
pagerConfig: {
autoHidden: true,
},
toolbarConfig: {},
});
const [Grid, gridApi] = useVbenVxeGrid({ gridOptions });
watch(
() => props.sessions,
(sessions) => {
nextTick(() => {
gridApi.setGridOptions({
data: sessions,
});
});
},
{
immediate: true,
},
);
function onDelete(session: IdentitySessionDto) {
emits('revoke', session);
}
</script>
<template>
<Grid>
<template #device="{ row }">
<div class="flex flex-row">
<span>{{ row.device }}</span>
<div class="pl-[5px]">
<Tag v-if="row.sessionId === getMySessionId" color="#87d068">
{{ $t('AbpIdentity.CurrentSession') }}
</Tag>
</div>
</div>
</template>
<template #deviceInfo="{ row }">
<Descriptions :colon="false" :column="2" bordered size="small">
<DescriptionItem
:label="$t('AbpIdentity.DisplayName:SessionId')"
:span="2"
>
{{ row.sessionId }}
</DescriptionItem>
<DescriptionItem :label="$t('AbpIdentity.DisplayName:Device')">
{{ row.device }}
</DescriptionItem>
<DescriptionItem :label="$t('AbpIdentity.DisplayName:DeviceInfo')">
{{ row.deviceInfo }}
</DescriptionItem>
<DescriptionItem :label="$t('AbpIdentity.DisplayName:ClientId')">
{{ row.clientId }}
</DescriptionItem>
<DescriptionItem :label="$t('AbpIdentity.DisplayName:IpAddresses')">
{{ row.ipAddresses }}
</DescriptionItem>
<DescriptionItem :label="$t('AbpIdentity.DisplayName:SignedIn')">
{{ row.signedIn }}
</DescriptionItem>
<DescriptionItem :label="$t('AbpIdentity.DisplayName:LastAccessed')">
{{ row.lastAccessed }}
</DescriptionItem>
</Descriptions>
</template>
<template #action="{ row }">
<div class="flex flex-row">
<Button
v-if="getAllowRevokeSession(row)"
danger
size="small"
@click="onDelete(row)"
>
{{ $t('AbpIdentity.RevokeSession') }}
</Button>
</div>
</template>
</Grid>
</template>
<style scoped></style>

67
apps/vben5/packages/@abp/identity/src/components/users/UserSessionDrawer.vue

@ -0,0 +1,67 @@
<script setup lang="ts">
import type { IdentitySessionDto, IdentityUserDto } from '../../types';
import { defineAsyncComponent, ref } from 'vue';
import { useVbenDrawer } from '@vben/common-ui';
import { $t } from '@vben/locales';
import { message, Modal } from 'ant-design-vue';
import { useUserSessionsApi } from '../../api/useUserSessionsApi';
const SessionTable = defineAsyncComponent(
() => import('../sessions/SessionTable.vue'),
);
const sessions = ref<IdentitySessionDto[]>([]);
const { cancel, getSessionsApi, revokeSessionApi } = useUserSessionsApi();
const [Drawer, drawerApi] = useVbenDrawer({
class: 'w-[800px]',
onBeforeClose: cancel,
onCancel() {
drawerApi.close();
},
onConfirm: async () => {},
onOpenChange: async (isOpen: boolean) => {
isOpen && (await onRefresh());
},
title: $t('AbpIdentity.IdentitySessions'),
});
async function onRefresh() {
try {
drawerApi.setState({ loading: true });
const dto = drawerApi.getData<IdentityUserDto>();
const { items } = await getSessionsApi({ userId: dto.id });
sessions.value = items;
} finally {
drawerApi.setState({ loading: false });
}
}
async function onRevoke(session: IdentitySessionDto) {
Modal.confirm({
centered: true,
content: $t('AbpIdentity.SessionWillBeRevokedMessage'),
iconType: 'warning',
onCancel: () => {
cancel();
},
onOk: async () => {
await revokeSessionApi(session.sessionId);
message.success($t('AbpIdentity.SuccessfullyRevoked'));
await onRefresh();
},
title: $t('AbpUi.AreYouSure'),
});
}
</script>
<template>
<Drawer>
<SessionTable :sessions="sessions" @revoke="onRevoke" />
</Drawer>
</template>
<style scoped></style>

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

@ -26,7 +26,10 @@ import {
import { Button, Dropdown, Menu, message, Modal } from 'ant-design-vue';
import { useUsersApi } from '../../api/useUsersApi';
import { IdentityUserPermissions } from '../../constants/permissions';
import {
IdentitySessionPermissions,
IdentityUserPermissions,
} from '../../constants/permissions';
defineOptions({
name: 'UserTable',
@ -46,6 +49,7 @@ const PasswordIcon = createIconifyIcon('carbon:password');
const MenuOutlined = createIconifyIcon('heroicons-outline:menu-alt-3');
const ClaimOutlined = createIconifyIcon('la:id-card-solid');
const PermissionsOutlined = createIconifyIcon('icon-park-outline:permissions');
const SessionIcon = createIconifyIcon('carbon:prompt-session');
const AuditLogIcon = createIconifyIcon('fluent-mdl2:compliance-audit');
const getLockEnd = computed(() => {
@ -166,6 +170,11 @@ const [UserPermissionModal, permissionModalApi] = useVbenModal({
const [UserChangeDrawer, userChangeDrawerApi] = useVbenDrawer({
connectedComponent: EntityChangeDrawer,
});
const [UserSessionDrawer, userSessionDrawerApi] = useVbenDrawer({
connectedComponent: defineAsyncComponent(
() => import('./UserSessionDrawer.vue'),
),
});
const [Grid, { query }] = useVbenVxeGrid({
formOptions,
gridEvents,
@ -240,6 +249,11 @@ const handleMenuClick = async (row: IdentityUserDto, info: MenuInfo) => {
permissionModalApi.open();
break;
}
case 'session': {
userSessionDrawerApi.setData(row);
userSessionDrawerApi.open();
break;
}
case 'unlock': {
handleUnlock(row);
break;
@ -330,6 +344,13 @@ const handleMenuClick = async (row: IdentityUserDto, info: MenuInfo) => {
>
{{ $t('AbpPermissionManagement.Permissions') }}
</MenuItem>
<MenuItem
v-if="hasAccessByCodes([IdentitySessionPermissions.Default])"
key="session"
:icon="h(SessionIcon)"
>
{{ $t('AbpIdentity.IdentitySessions') }}
</MenuItem>
<MenuItem
v-if="
hasAccessByCodes([IdentityUserPermissions.ManageClaims])
@ -376,6 +397,7 @@ const handleMenuClick = async (row: IdentityUserDto, info: MenuInfo) => {
<UserEditModal @change="() => query()" />
<UserPasswordModal @change="query" />
<UserPermissionModal />
<UserSessionDrawer />
<UserChangeDrawer />
</template>

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

@ -46,6 +46,12 @@ export const SecurityLogPermissions = {
/** 删除 */
Delete: 'AbpAuditing.SecurityLog.Delete',
};
/** 用户会话权限 */
export const IdentitySessionPermissions = {
Default: 'AbpIdentity.IdentitySessions',
/** 移除 */
Revoke: 'AbpIdentity.IdentitySessions.Revoke',
};
/**
*
* @deprecated 使.

1
apps/vben5/packages/@abp/identity/src/types/index.ts

@ -2,4 +2,5 @@ export * from './claim-types';
export * from './organization-units';
export * from './roles';
export * from './security-logs';
export * from './sessions';
export * from './users';

25
apps/vben5/packages/@abp/identity/src/types/sessions.ts

@ -0,0 +1,25 @@
import type { EntityDto, PagedAndSortedResultRequestDto } from '@abp/core';
interface IdentitySessionDto extends EntityDto<string> {
clientId?: string;
device: string;
deviceInfo: string;
ipAddresses?: string;
lastAccessed?: Date;
sessionId: string;
signedIn: Date;
userId: string;
}
interface GetUserSessionsInput extends PagedAndSortedResultRequestDto {
clientId?: string;
device?: string;
userId?: string;
}
interface GetMySessionsInput extends PagedAndSortedResultRequestDto {
clientId?: string;
device?: string;
}
export type { GetMySessionsInput, GetUserSessionsInput, IdentitySessionDto };
Loading…
Cancel
Save