Browse Source

🚧 feat(session): 处理会话过期事件.

pull/1087/head
colin 1 year ago
parent
commit
4c469a3f6a
  1. 1
      apps/vben5/apps/app-antd/package.json
  2. 48
      apps/vben5/apps/app-antd/src/hooks/useSessions.ts
  3. 3
      apps/vben5/apps/app-antd/src/layouts/basic.vue
  4. 7
      apps/vben5/apps/app-antd/src/store/auth.ts
  5. 12
      apps/vben5/apps/app-antd/vite.config.mts
  6. 10
      apps/vben5/packages/@abp/core/src/constants/events.ts
  7. 1
      apps/vben5/packages/@abp/core/src/constants/index.ts
  8. 1
      apps/vben5/packages/@abp/core/src/hooks/index.ts
  9. 30
      apps/vben5/packages/@abp/core/src/hooks/useEventBus.ts
  10. 1
      apps/vben5/packages/@abp/core/src/utils/index.ts
  11. 107
      apps/vben5/packages/@abp/core/src/utils/mitt.ts
  12. 1
      apps/vben5/packages/@abp/notifications/package.json
  13. 1
      apps/vben5/packages/@abp/notifications/src/constants/index.ts
  14. 4
      apps/vben5/packages/@abp/notifications/src/constants/notifications.ts
  15. 2
      apps/vben5/packages/@abp/notifications/src/hooks/index.ts
  16. 56
      apps/vben5/packages/@abp/notifications/src/hooks/useNotificationSerializer.ts
  17. 113
      apps/vben5/packages/@abp/notifications/src/hooks/useNotifications.ts
  18. 1
      apps/vben5/packages/@abp/notifications/src/index.ts
  19. 14
      apps/vben5/packages/@abp/notifications/src/types/notifications.ts
  20. 27
      apps/vben5/packages/@abp/signalr/package.json
  21. 1
      apps/vben5/packages/@abp/signalr/src/hooks/index.ts
  22. 155
      apps/vben5/packages/@abp/signalr/src/hooks/useSignalR.ts
  23. 1
      apps/vben5/packages/@abp/signalr/src/index.ts
  24. 6
      apps/vben5/packages/@abp/signalr/tsconfig.json
  25. 1
      apps/vben5/pnpm-workspace.yaml

1
apps/vben5/apps/app-antd/package.json

@ -30,6 +30,7 @@
"@abp/auditing": "workspace:*",
"@abp/core": "workspace:*",
"@abp/identity": "workspace:*",
"@abp/notifications": "workspace:*",
"@abp/openiddict": "workspace:*",
"@abp/permissions": "workspace:*",
"@abp/request": "workspace:*",

48
apps/vben5/apps/app-antd/src/hooks/useSessions.ts

@ -0,0 +1,48 @@
/* eslint-disable @typescript-eslint/no-non-null-assertion */
import type { Notification as NotificationInfo } from '@abp/notifications';
import { onMounted, onUnmounted } from 'vue';
import { useAbpStore, useEventBus } from '@abp/core';
import { NotificationNames, useNotifications } from '@abp/notifications';
import { Modal } from 'ant-design-vue';
import { useAuthStore } from '#/store';
export function useSessions() {
const authStore = useAuthStore();
const abpStore = useAbpStore();
const { subscribe, unSubscribe } = useEventBus();
const { register, release } = useNotifications();
function _register() {
subscribe(NotificationNames.SessionExpiration, _onSessionExpired);
register();
}
function _release() {
unSubscribe(NotificationNames.SessionExpiration, _onSessionExpired);
release();
}
/** 处理会话过期事件 */
function _onSessionExpired(event?: NotificationInfo) {
const { data, title, message } = event!;
const sessionId = data.SessionId;
if (sessionId === abpStore.application?.currentUser?.sessionId) {
_release();
Modal.confirm({
iconType: 'warning',
title,
content: message,
centered: true,
afterClose: () => {
authStore.logout();
},
});
}
}
onMounted(_register);
onUnmounted(_release);
}

3
apps/vben5/apps/app-antd/src/layouts/basic.vue

@ -23,6 +23,7 @@ import { preferences } from '@vben/preferences';
import { useAccessStore, useUserStore } from '@vben/stores';
import { openWindow } from '@vben/utils';
import { useSessions } from '#/hooks/useSessions';
import { $t } from '#/locales';
import { useAuthStore } from '#/store';
import LoginForm from '#/views/_core/authentication/login.vue';
@ -60,6 +61,8 @@ const notifications = ref<NotificationItem[]>([
},
]);
useSessions();
const { replace } = useRouter();
const userStore = useUserStore();
const authStore = useAuthStore();

7
apps/vben5/apps/app-antd/src/store/auth.ts

@ -7,7 +7,7 @@ import { DEFAULT_HOME_PATH, LOGIN_PATH } from '@vben/constants';
import { resetAllStores, useAccessStore, useUserStore } from '@vben/stores';
import { useTokenApi, useUserInfoApi } from '@abp/account';
import { useAbpStore } from '@abp/core';
import { Events, useAbpStore, useEventBus } from '@abp/core';
import { notification } from 'ant-design-vue';
import { defineStore } from 'pinia';
@ -15,6 +15,7 @@ import { useAbpConfigApi } from '#/api/core/useAbpConfigApi';
import { $t } from '#/locales';
export const useAuthStore = defineStore('auth', () => {
const { publish } = useEventBus();
const { loginApi } = useTokenApi();
const { getUserInfoApi } = useUserInfoApi();
const { getConfigApi } = useAbpConfigApi();
@ -49,6 +50,8 @@ export const useAuthStore = defineStore('auth', () => {
userStore.setUserInfo(userInfo);
publish(Events.UserLogin, userInfo);
if (accessStore.loginExpired) {
accessStore.setLoginExpired(false);
} else {
@ -83,6 +86,8 @@ export const useAuthStore = defineStore('auth', () => {
resetAllStores();
accessStore.setLoginExpired(false);
publish(Events.UserLogout);
// 回登录页带上当前路由地址
await router.replace({
path: LOGIN_PATH,

12
apps/vben5/apps/app-antd/vite.config.mts

@ -8,22 +8,18 @@ export default defineConfig(async () => {
proxy: {
'/.well-known': {
changeOrigin: true,
// rewrite: (path) => path.replace(/^\/api/, ''),
// mock代理目标地址
target: 'http://127.0.0.1:30001/',
ws: true,
},
'/api': {
changeOrigin: true,
// rewrite: (path) => path.replace(/^\/api/, ''),
// mock代理目标地址
target: 'http://127.0.0.1:30001/',
ws: true,
},
'/connect': {
changeOrigin: true,
// rewrite: (path) => path.replace(/^\/api/, ''),
// mock代理目标地址
target: 'http://127.0.0.1:30001/',
},
'/signalr-hubs': {
changeOrigin: true,
target: 'http://127.0.0.1:30001/',
ws: true,
},

10
apps/vben5/packages/@abp/core/src/constants/events.ts

@ -0,0 +1,10 @@
export const Events = {
/** 收到服务器消息 */
GetNotification: 'get-notification',
/** 新通知消息 */
NotificationRecevied: 'sys_notifications_recevied',
/** 用户登录事件 */
UserLogin: 'sys_user_login',
/** 用户登出事件 */
UserLogout: 'sys_user_logout',
};

1
apps/vben5/packages/@abp/core/src/constants/index.ts

@ -1 +1,2 @@
export * from './events';
export * from './validation';

1
apps/vben5/packages/@abp/core/src/hooks/index.ts

@ -1,4 +1,5 @@
export * from './useAuthorization';
export * from './useEventBus';
export * from './useFeatures';
export * from './useGlobalFeatures';
export * from './useLocalization';

30
apps/vben5/packages/@abp/core/src/hooks/useEventBus.ts

@ -0,0 +1,30 @@
import type { EventType, Handler, WildcardHandler } from '../utils/mitt';
import mitt from '../utils/mitt';
const emitter = mitt();
interface EventBus {
/** 发布事件 */
publish(type: '*', event?: any): void;
/** 发布事件 */
publish<T = any>(type: EventType, event?: T): void;
/** 订阅事件 */
subscribe(type: '*', handler: WildcardHandler): void;
/** 订阅事件 */
subscribe<T = any>(type: EventType, handler: Handler<T>): void;
/** 退订事件 */
unSubscribe(type: '*', handler: WildcardHandler): void;
/** 退订事件 */
unSubscribe<T = any>(type: EventType, handler: Handler<T>): void;
}
export function useEventBus(): EventBus {
return {
publish: emitter.emit,
subscribe: emitter.on,
unSubscribe: emitter.off,
};
}

1
apps/vben5/packages/@abp/core/src/utils/index.ts

@ -1,4 +1,5 @@
export * from './date';
export * from './mitt';
export * from './regex';
export * from './string';
export * from './tree';

107
apps/vben5/packages/@abp/core/src/utils/mitt.ts

@ -0,0 +1,107 @@
/* eslint-disable array-callback-return */
/**
* copy to https://github.com/developit/mitt
* Expand clear method
*/
export type EventType = string | symbol;
// An event handler can take an optional event argument
// and should not return a value
export type Handler<T = any> = (event?: T) => void;
export type WildcardHandler = (type: EventType, event?: any) => void;
// An array of all currently registered event handlers for a type
export type EventHandlerList = Array<Handler>;
export type WildCardEventHandlerList = Array<WildcardHandler>;
// A map of event types and their corresponding event handlers.
export type EventHandlerMap = Map<
EventType,
EventHandlerList | WildCardEventHandlerList
>;
export interface Emitter {
all: EventHandlerMap;
clear(): void;
emit(type: '*', event?: any): void;
emit<T = any>(type: EventType, event?: T): void;
off(type: '*', handler: WildcardHandler): void;
off<T = any>(type: EventType, handler: Handler<T>): void;
on(type: '*', handler: WildcardHandler): void;
on<T = any>(type: EventType, handler: Handler<T>): void;
}
/**
* Mitt: Tiny (~200b) functional event emitter / pubsub.
* @name mitt
* @returns {Mitt} Emitter
*/
export default function mitt(all?: EventHandlerMap): Emitter {
all = all || new Map();
return {
/**
* A Map of event names to registered handler functions.
*/
all,
/**
* Clear all
*/
clear() {
this.all.clear();
},
/**
* Invoke all handlers for the given type.
* If present, `"*"` handlers are invoked after type-matched handlers.
*
* Note: Manually firing "*" handlers is not supported.
*
* @param {string|symbol} type The event type to invoke
* @param {Any} [evt] Any value (object is recommended and powerful), passed to each handler
* @memberOf mitt
*/
emit<T = any>(type: EventType, evt: T) {
[...((all?.get(type) || []) as EventHandlerList)].map((handler) => {
handler(evt);
});
[...((all?.get('*') || []) as WildCardEventHandlerList)].map(
(handler) => {
handler(type, evt);
},
);
},
/**
* Remove an event handler for the given type.
* @param {string|symbol} type Type of event to unregister `handler` from, or `"*"`
* @param {Function} handler Handler function to remove
* @memberOf mitt
*/
off<T = any>(type: EventType, handler: Handler<T>) {
const handlers = all?.get(type);
if (handlers) {
handlers.splice(handlers.indexOf(handler) >>> 0, 1);
}
},
/**
* Register an event handler for the given type.
* @param {string|symbol} type Type of event to listen for, or `"*"` for all events
* @param {Function} handler Function to call in response to given event
* @memberOf mitt
*/
on<T = any>(type: EventType, handler: Handler<T>) {
const handlers = all?.get(type);
const added = handlers && handlers.push(handler);
if (!added) {
all?.set(type, [handler]);
}
},
};
}

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

@ -22,6 +22,7 @@
"dependencies": {
"@abp/core": "workspace:*",
"@abp/request": "workspace:*",
"@abp/signalr": "workspace:*",
"@abp/ui": "workspace:*",
"@ant-design/icons-vue": "catalog:",
"@vben/access": "workspace:*",

1
apps/vben5/packages/@abp/notifications/src/constants/index.ts

@ -0,0 +1 @@
export * from './notifications';

4
apps/vben5/packages/@abp/notifications/src/constants/notifications.ts

@ -0,0 +1,4 @@
export const NotificationNames = {
/** 会话过期通知 */
SessionExpiration: 'AbpIdentity.Session.Expiration',
};

2
apps/vben5/packages/@abp/notifications/src/hooks/index.ts

@ -0,0 +1,2 @@
export * from './useNotifications';
export * from './useNotificationSerializer';

56
apps/vben5/packages/@abp/notifications/src/hooks/useNotificationSerializer.ts

@ -0,0 +1,56 @@
import type { Notification, NotificationInfo } from '../types';
import { useLocalization } from '@abp/core';
export function useNotificationSerializer() {
function deserialize(notificationInfo: NotificationInfo): Notification {
const { data } = notificationInfo;
let title = data.extraProperties.title;
let message = data.extraProperties.message;
let description = data.extraProperties.description;
if (data.extraProperties.L === true || data.extraProperties.L === 'true') {
const { L } = useLocalization([
data.extraProperties.title.resourceName ??
data.extraProperties.title.ResourceName,
data.extraProperties.message.resourceName ??
data.extraProperties.message.ResourceName,
data.extraProperties.description?.resourceName ??
data.extraProperties.description?.ResourceName ??
'AbpUi',
]);
title = L(
data.extraProperties.title.name ?? data.extraProperties.title.Name,
data.extraProperties.title.values ?? data.extraProperties.title.Values,
);
message = L(
data.extraProperties.message.name ?? data.extraProperties.message.Name,
data.extraProperties.message.values ??
data.extraProperties.message.Values,
);
if (description) {
description = L(
data.extraProperties.description.name ??
data.extraProperties.description.Name,
data.extraProperties.description.values ??
data.extraProperties.description.Values,
);
}
}
return {
contentType: notificationInfo.contentType,
creationTime: notificationInfo.creationTime,
data: data.extraProperties,
description,
lifetime: notificationInfo.lifetime,
message,
name: notificationInfo.name,
severity: notificationInfo.severity,
title,
type: notificationInfo.type,
};
}
return {
deserialize,
};
}

113
apps/vben5/packages/@abp/notifications/src/hooks/useNotifications.ts

@ -0,0 +1,113 @@
import type { Notification, NotificationInfo } from '../types/notifications';
import { Events, useEventBus } from '@abp/core';
import { useSignalR } from '@abp/signalr';
import { notification } from 'ant-design-vue';
import {
NotificationContentType,
NotificationSeverity,
NotificationType,
} from '../types/notifications';
import { useNotificationSerializer } from './useNotificationSerializer';
export function useNotifications() {
const { deserialize } = useNotificationSerializer();
const { publish, subscribe, unSubscribe } = useEventBus();
const { init, off, on, onStart, stop } = useSignalR();
/** 注册通知 */
function register() {
_registerEvents();
_init();
}
/** 释放通知 */
function release() {
_releaseEvents();
stop();
}
function _init() {
onStart(() => on(Events.GetNotification, _onNotifyReceived));
init({
autoStart: true,
serverUrl: '/signalr-hubs/notifications',
useAccessToken: true,
});
}
/** 注册通知事件 */
function _registerEvents() {
subscribe(Events.UserLogout, stop);
}
/** 释放通知事件 */
function _releaseEvents() {
unSubscribe(Events.UserLogout, stop);
off(Events.GetNotification, _onNotifyReceived);
}
/** 接收通知回调 */
function _onNotifyReceived(notificationInfo: NotificationInfo) {
const notification = deserialize(notificationInfo);
if (notification.type === NotificationType.ServiceCallback) {
publish(notification.name, notification);
return;
}
publish(Events.NotificationRecevied, notification);
_notification(notification);
}
/** 通知推送 */
function _notification(notifier: Notification) {
let message = notifier.description;
switch (notifier.contentType) {
case NotificationContentType.Html:
case NotificationContentType.Json:
case NotificationContentType.Markdown: {
message = notifier.title;
break;
}
case NotificationContentType.Text: {
message = notifier.description;
}
}
switch (notifier.severity) {
case NotificationSeverity.Error:
case NotificationSeverity.Fatal: {
notification.error({
description: message,
message: notifier.title,
});
break;
}
case NotificationSeverity.Info: {
notification.info({
description: message,
message: notifier.title,
});
break;
}
case NotificationSeverity.Success: {
notification.success({
description: message,
message: notifier.title,
});
break;
}
case NotificationSeverity.Warn: {
notification.warning({
description: message,
message: notifier.title,
});
break;
}
}
}
return {
register,
release,
};
}

1
apps/vben5/packages/@abp/notifications/src/index.ts

@ -1,4 +1,5 @@
export * from './api';
export * from './components';
export * from './constants';
export * from './hooks';
export * from './types';

14
apps/vben5/packages/@abp/notifications/src/types/notifications.ts

@ -75,7 +75,21 @@ interface NotificationGroupDto {
notifications: NotificationDto[];
}
interface Notification {
contentType: NotificationContentType;
creationTime: Date;
data: Record<string, any>;
description?: string;
lifetime: NotificationLifetime;
message: string;
name: string;
severity: NotificationSeverity;
title: string;
type: NotificationType;
}
export type {
Notification,
NotificationData,
NotificationDto,
NotificationGroupDto,

27
apps/vben5/packages/@abp/signalr/package.json

@ -0,0 +1,27 @@
{
"name": "@abp/signalr",
"version": "8.3.2",
"homepage": "https://github.com/colinin/abp-next-admin",
"bugs": "https://github.com/colinin/abp-next-admin/issues",
"repository": {
"type": "git",
"url": "git+https://github.com/colinin/abp-next-admin.git",
"directory": "packages/@abp/signalr"
},
"license": "MIT",
"type": "module",
"sideEffects": [
"**/*.css"
],
"exports": {
".": {
"types": "./src/index.ts",
"default": "./src/index.ts"
}
},
"dependencies": {
"@abp/core": "workspace:*",
"@microsoft/signalr": "catalog:",
"@vben/stores": "workspace:*"
}
}

1
apps/vben5/packages/@abp/signalr/src/hooks/index.ts

@ -0,0 +1 @@
export * from './useSignalR';

155
apps/vben5/packages/@abp/signalr/src/hooks/useSignalR.ts

@ -0,0 +1,155 @@
/* eslint-disable @typescript-eslint/no-non-null-assertion */
import type { HubConnection, IHttpConnectionOptions } from '@microsoft/signalr';
import { useAccessStore } from '@vben/stores';
import { useEventBus } from '@abp/core';
import { HubConnectionBuilder, LogLevel } from '@microsoft/signalr';
interface SignalROptions {
/** 断线自动重连 */
automaticReconnect?: boolean;
/** 初始化自动建立连接 */
autoStart?: boolean;
/** 下次重试间隔(ms) */
nextRetryDelayInMilliseconds?: number;
/** 服务端url */
serverUrl: string;
/** 是否携带访问令牌 */
useAccessToken?: boolean;
}
export function useSignalR() {
const { publish, subscribe } = useEventBus();
let connection: HubConnection | null = null;
/** 初始化SignalR */
async function init({
automaticReconnect = true,
autoStart = false,
nextRetryDelayInMilliseconds = 60_000,
serverUrl,
useAccessToken = true,
}: SignalROptions) {
const httpOptions: IHttpConnectionOptions = {};
if (useAccessToken) {
const accessStore = useAccessStore();
const token = accessStore.accessToken;
if (token) {
httpOptions.accessTokenFactory = () =>
token.startsWith('Bearer ') ? token.slice(7) : token;
}
}
const connectionBuilder = new HubConnectionBuilder()
.withUrl(serverUrl, httpOptions)
.configureLogging(LogLevel.Warning);
if (automaticReconnect && nextRetryDelayInMilliseconds) {
connectionBuilder.withAutomaticReconnect({
nextRetryDelayInMilliseconds: () => nextRetryDelayInMilliseconds,
});
}
connection = connectionBuilder.build();
if (autoStart) {
await start();
}
}
/** 启动连接 */
async function start(): Promise<void> {
_throwIfNotInit();
publish('signalR:beforeStart');
try {
await connection!.start();
publish('signalR:onStart');
} catch (error) {
publish('signalR:onError', error);
}
}
/** 关闭连接 */
async function stop(): Promise<void> {
_throwIfNotInit();
publish('signalR:beforeStop');
try {
await connection!.stop();
publish('signalR:onStop');
} catch (error) {
publish('signalR:onError', error);
}
}
/** 连接前事件 */
function beforeStart<T = any>(callback: (event?: T) => void) {
subscribe('signalR:beforeStart', callback);
}
/** 连接后事件 */
function onStart<T = any>(callback: (event?: T) => void) {
subscribe('signalR:onStart', callback);
}
/** 关闭连接前事件 */
function beforeStop<T = any>(callback: (event?: T) => void) {
subscribe('signalR:beforeStop', callback);
}
/** 关闭连接后事件 */
function onStop<T = any>(callback: (event?: T) => void) {
subscribe('signalR:onStop', callback);
}
/** 连接错误事件 */
function onError(callback: (error?: Error) => void) {
subscribe('signalR:onError', callback);
}
/** 订阅服务端消息 */
function on(methodName: string, newMethod: (...args: any[]) => void): void {
connection?.on(methodName, newMethod);
}
/** 注销服务端消息 */
function off(methodName: string, method: (...args: any[]) => void): void {
connection?.off(methodName, method);
}
/** 连接关闭事件 */
function onClose(callback: (error?: Error) => void): void {
connection?.onclose(callback);
}
/** 发送消息 */
function send(methodName: string, ...args: any[]): Promise<void> {
_throwIfNotInit();
return connection!.send(methodName, ...args);
}
/** 调用函数 */
function invoke<T = any>(methodName: string, ...args: any[]): Promise<T> {
_throwIfNotInit();
return connection!.invoke(methodName, ...args);
}
function _throwIfNotInit() {
if (connection === null) {
throw new Error('unable to send message, connection not initialized!');
}
}
return {
beforeStart,
beforeStop,
init,
invoke,
off,
on,
onClose,
onError,
onStart,
onStop,
send,
start,
stop,
};
}

1
apps/vben5/packages/@abp/signalr/src/index.ts

@ -0,0 +1 @@
export * from './hooks';

6
apps/vben5/packages/@abp/signalr/tsconfig.json

@ -0,0 +1,6 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"extends": "@vben/tsconfig/web.json",
"include": ["src"],
"exclude": ["node_modules"]
}

1
apps/vben5/pnpm-workspace.yaml

@ -32,6 +32,7 @@ catalog:
'@intlify/unplugin-vue-i18n': ^6.0.1
'@jspm/generator': ^2.4.1
'@manypkg/get-packages': ^2.2.2
'@microsoft/signalr': ^8.0.7
'@nolebase/vitepress-plugin-git-changelog': ^2.11.1
'@playwright/test': ^1.49.1
'@pnpm/workspace.read-manifest': ^1000.0.1

Loading…
Cancel
Save