diff --git a/backend/IM_API/Hubs/ChatHub.cs b/backend/IM_API/Hubs/ChatHub.cs index 12cb1b4..960af7e 100644 --- a/backend/IM_API/Hubs/ChatHub.cs +++ b/backend/IM_API/Hubs/ChatHub.cs @@ -42,7 +42,7 @@ namespace IM_API.Hubs } var userIdStr = Context.User.FindFirstValue(ClaimTypes.NameIdentifier); await _messageService.SendPrivateMessageAsync(int.Parse(userIdStr),dto.ReceiverId,dto); - await Clients.Users(dto.ReceiverId.ToString()).SendAsync("ReceiveMessage", userIdStr, dto.Content); + await Clients.Users(dto.ReceiverId.ToString()).SendAsync("ReceiveMessage", dto); return; } } diff --git a/backend/IM_API/Program.cs b/backend/IM_API/Program.cs index 9695881..4b6c5a5 100644 --- a/backend/IM_API/Program.cs +++ b/backend/IM_API/Program.cs @@ -44,7 +44,13 @@ namespace IM_API policy.AllowAnyHeader() .AllowAnyMethod() .AllowAnyHeader() - .AllowAnyOrigin(); + .AllowCredentials() + .SetIsOriginAllowed(origin => + { + // 允许所有来自本地或特定网段的请求 + var host = new Uri(origin).Host; + return host == "localhost" || host.StartsWith("192.168."); + }); }); }); //凭证处理 @@ -86,11 +92,7 @@ namespace IM_API context.Token = accessToken; } return Task.CompletedTask; - } - }; - - options.Events = new JwtBearerEvents - { + }, OnAuthenticationFailed = context => { Console.WriteLine("Authentication failed: " + context.Exception.Message); diff --git a/frontend/web/.env b/frontend/web/.env index ba350c4..e727f6c 100644 --- a/frontend/web/.env +++ b/frontend/web/.env @@ -1 +1,2 @@ -VITE_API_BASE_URL = http://192.168.5.116:7070/api \ No newline at end of file +VITE_API_BASE_URL = http://localhost:5202/api +VITE_SIGNALR_BASE_URL = http://localhost:5202/chat \ No newline at end of file diff --git a/frontend/web/package-lock.json b/frontend/web/package-lock.json index 5fa7968..2069b46 100644 --- a/frontend/web/package-lock.json +++ b/frontend/web/package-lock.json @@ -8,10 +8,12 @@ "name": "web", "version": "0.0.0", "dependencies": { + "@microsoft/signalr": "^10.0.0", "@vuelidate/core": "^2.0.3", "@vuelidate/validators": "^2.0.4", "axios": "^1.13.2", "feather-icons": "^4.29.2", + "idb": "^8.0.3", "pinia": "^3.0.3", "vee-validate": "^4.15.1", "vue": "^3.5.22", @@ -1458,6 +1460,40 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@microsoft/signalr": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@microsoft/signalr/-/signalr-10.0.0.tgz", + "integrity": "sha512-0BRqz/uCx3JdrOqiqgFhih/+hfTERaUfCZXFB52uMaZJrKaPRzHzMuqVsJC/V3pt7NozcNXGspjKiQEK+X7P2w==", + "license": "MIT", + "dependencies": { + "abort-controller": "^3.0.0", + "eventsource": "^2.0.2", + "fetch-cookie": "^2.0.3", + "node-fetch": "^2.6.7", + "ws": "^7.5.10" + } + }, + "node_modules/@microsoft/signalr/node_modules/ws": { + "version": "7.5.10", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", + "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", + "license": "MIT", + "engines": { + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -2613,6 +2649,18 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "license": "MIT", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, "node_modules/acorn": { "version": "8.15.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", @@ -3716,6 +3764,24 @@ "node": ">=0.10.0" } }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/eventsource": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-2.0.2.tgz", + "integrity": "sha512-IzUmBGPR3+oUG9dUeXynyNmf91/3zUSJg1lCktzKw47OXuhco54U3r9B7O4XX+Rb1Itm9OZ2b0RkTs10bICOxA==", + "license": "MIT", + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/execa": { "version": "9.6.0", "resolved": "https://registry.npmjs.org/execa/-/execa-9.6.0.tgz", @@ -3831,6 +3897,31 @@ "core-js": "^3.1.3" } }, + "node_modules/fetch-cookie": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/fetch-cookie/-/fetch-cookie-2.2.0.tgz", + "integrity": "sha512-h9AgfjURuCgA2+2ISl8GbavpUdR+WGAM2McW/ovn4tVccegp8ZqCKWSBR8uRdM8dDNlx5WdKRWxBYUwteLDCNQ==", + "license": "Unlicense", + "dependencies": { + "set-cookie-parser": "^2.4.8", + "tough-cookie": "^4.0.0" + } + }, + "node_modules/fetch-cookie/node_modules/tough-cookie": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz", + "integrity": "sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==", + "license": "BSD-3-Clause", + "dependencies": { + "psl": "^1.1.33", + "punycode": "^2.1.1", + "universalify": "^0.2.0", + "url-parse": "^1.5.3" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/figures": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/figures/-/figures-6.1.0.tgz", @@ -4230,6 +4321,12 @@ "node": ">=0.10.0" } }, + "node_modules/idb": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/idb/-/idb-8.0.3.tgz", + "integrity": "sha512-LtwtVyVYO5BqRvcsKuB2iUMnHwPVByPCXFXOpuU96IZPPoPN6xjOGxZQ74pgSVVLQWtUOYgyeL4GE98BY5D3wg==", + "license": "ISC" + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -4813,6 +4910,48 @@ "dev": true, "license": "MIT" }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/node-fetch/node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, + "node_modules/node-fetch/node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, + "node_modules/node-fetch/node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, "node_modules/node-releases": { "version": "2.0.23", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.23.tgz", @@ -5224,16 +5363,33 @@ "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", "license": "MIT" }, + "node_modules/psl": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.15.0.tgz", + "integrity": "sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==", + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "funding": { + "url": "https://github.com/sponsors/lupomontero" + } + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "dev": true, "license": "MIT", "engines": { "node": ">=6" } }, + "node_modules/querystringify": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", + "license": "MIT" + }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -5265,6 +5421,12 @@ "node": ">=0.10.0" } }, + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", + "license": "MIT" + }, "node_modules/resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", @@ -5411,6 +5573,12 @@ "node": ">=10" } }, + "node_modules/set-cookie-parser": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", + "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", + "license": "MIT" + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -5938,6 +6106,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/universalify": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", + "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", + "license": "MIT", + "engines": { + "node": ">= 4.0.0" + } + }, "node_modules/unplugin-utils": { "version": "0.3.1", "resolved": "https://registry.npmjs.org/unplugin-utils/-/unplugin-utils-0.3.1.tgz", @@ -6009,6 +6186,16 @@ "punycode": "^2.1.0" } }, + "node_modules/url-parse": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", + "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", + "license": "MIT", + "dependencies": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", diff --git a/frontend/web/package.json b/frontend/web/package.json index 3897f07..0ca5cdd 100644 --- a/frontend/web/package.json +++ b/frontend/web/package.json @@ -15,10 +15,12 @@ "format": "prettier --write src/" }, "dependencies": { + "@microsoft/signalr": "^10.0.0", "@vuelidate/core": "^2.0.3", "@vuelidate/validators": "^2.0.4", "axios": "^1.13.2", "feather-icons": "^4.29.2", + "idb": "^8.0.3", "pinia": "^3.0.3", "vee-validate": "^4.15.1", "vue": "^3.5.22", diff --git a/frontend/web/src/stores/chat.js b/frontend/web/src/stores/chat.js new file mode 100644 index 0000000..e69de29 diff --git a/frontend/web/src/stores/signalr.js b/frontend/web/src/stores/signalr.js new file mode 100644 index 0000000..124be1d --- /dev/null +++ b/frontend/web/src/stores/signalr.js @@ -0,0 +1,44 @@ +import { defineStore } from "pinia"; +import * as signalR from '@microsoft/signalr'; +import { useMessage } from "@/components/messages/useAlert"; +import { useAuthStore } from "./auth"; + +const message = useMessage() +const authStore = useAuthStore() + +export const useSignalRStore = defineStore('signalr', { + state: () => ({ + connection: null, + isConnected: false + }), + actions: { + async initSignalR() { + const url = import.meta.env.VITE_SIGNALR_BASE_URL || 'http://localhost:5202/chat'; + this.connection = new signalR.HubConnectionBuilder() + .withUrl(url, { + accessTokenFactory: () => { + return authStore.token; + } + }) + .withAutomaticReconnect() + .build(); + this.registerHandlers(); + try { + await this.connection.start(); + this.isConnected = true; + console.log('SignalR寤虹珛閫氫俊鎴愬姛锛') + } catch (e) { + message.error('涓庢湇鍔″櫒寤虹珛閫氫俊澶辫触锛岃妫鏌ョ綉缁滆繛鎺...'); + } + }, + registerHandlers() { + this.connection.on('ReceiveMessage', (msg) => { + console.log(msg) + }); + + this.connection.onclose(() => { this.isConnected = false }); + this.connection.onreconnected(() => { this.isConnected = true }); + + } + } +}) \ No newline at end of file diff --git a/frontend/web/src/utils/db/messageDB.js b/frontend/web/src/utils/db/messageDB.js new file mode 100644 index 0000000..86b824d --- /dev/null +++ b/frontend/web/src/utils/db/messageDB.js @@ -0,0 +1,26 @@ +import { openDB } from "idb"; + +const DBNAME = 'IM_DB'; +const STORE_NAME = 'messages'; + +export const dbPromise = openDB(DBNAME, 1, { + upgrade(db) { + if (!db.objectStoreNames.contains(STORE_NAME)) { + const store = db.createObjectStore(STORE_NAME, { keyPath: 'msgId' }); + store.createIndex('by-sessionId', 'sessionId'); + store.createIndex('by-time', 'timeStamp'); + } + } +}) + +export const messagesDb = { + async save(msg) { + return (await dbPromise).put(STORE_NAME, msg); + }, + async getBySession(sessionId) { + return (await dbPromise).getAllFromIndex(STORE_NAME, 'by-sessionId', sessionId); + }, + async clearAll() { + return (await dbPromise).clear(STORE_NAME); + } +} \ No newline at end of file diff --git a/frontend/web/src/views/auth/Login.vue b/frontend/web/src/views/auth/Login.vue index c4803a6..acf9ded 100644 --- a/frontend/web/src/views/auth/Login.vue +++ b/frontend/web/src/views/auth/Login.vue @@ -57,10 +57,12 @@ import MyButton from '@/components/MyButton.vue' import { required, maxLength, helpers } from '@vuelidate/validators' import useVuelidate from '@vuelidate/core' import { useAuthStore } from '@/stores/auth' +import { useSignalRStore } from '@/stores/signalr' const message = useMessage(); const router = useRouter(); const authStore = useAuthStore(); +const signalRStore = useSignalRStore(); const loading = ref(false) const form = reactive({ @@ -96,6 +98,7 @@ const handleLogin = async () => { if(res.code === 0){ // Assuming 0 is success message.success('鐧诲綍鎴愬姛') authStore.setLoginInfo(res.data.token, res.data.refreshToken, res.data.userInfo); + signalRStore.initSignalR(res.data.token); router.push('/messages') }else{ message.error(res.message || '鐧诲綍澶辫触')