diff --git a/Wordle.Api/Wordle.Api/Program.cs b/Wordle.Api/Wordle.Api/Program.cs index d196997b..94ba6517 100644 --- a/Wordle.Api/Wordle.Api/Program.cs +++ b/Wordle.Api/Wordle.Api/Program.cs @@ -3,7 +3,6 @@ using Microsoft.EntityFrameworkCore; using Microsoft.IdentityModel.Tokens; using Microsoft.OpenApi.Models; -using System; using System.Text; using Wordle.Api; using Wordle.Api.Identity; @@ -24,17 +23,20 @@ { options.AddPolicy(name: AllOrigins, policy => { - policy.WithOrigins("*"); + policy.WithOrigins("http://localhost:3000"); policy.AllowAnyMethod(); policy.AllowAnyHeader(); + policy.AllowCredentials(); }); }); builder.Services.AddControllers(); // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle builder.Services.AddEndpointsApiExplorer(); +builder.Services.AddSignalR(); builder.Services.AddSwaggerGen(config => { + config.AddSignalRSwaggerGen(); config.SwaggerDoc("v1", new OpenApiInfo { Title = "Wordle API", Version = "v1" }); config.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme { @@ -63,6 +65,7 @@ builder.Services.AddScoped(); builder.Services.AddScoped(); +builder.Services.AddScoped(); // Identity Services builder.Services.AddIdentityCore(options => options.SignIn.RequireConfirmedAccount = false) @@ -128,6 +131,10 @@ await IdentitySeed.SeedAsync( app.MapControllers(); +app.UseRouting(); + +app.MapHub("hub"); + app.Run(); public partial class Program { } \ No newline at end of file diff --git a/Wordle.Api/Wordle.Api/Wordle.Api.csproj b/Wordle.Api/Wordle.Api/Wordle.Api.csproj index 5355197d..bcdba87a 100644 --- a/Wordle.Api/Wordle.Api/Wordle.Api.csproj +++ b/Wordle.Api/Wordle.Api/Wordle.Api.csproj @@ -8,14 +8,17 @@ + + all runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/Wordle.Api/Wordle.Api/WordleHub.cs b/Wordle.Api/Wordle.Api/WordleHub.cs new file mode 100644 index 00000000..c34a40c3 --- /dev/null +++ b/Wordle.Api/Wordle.Api/WordleHub.cs @@ -0,0 +1,14 @@ +using Microsoft.AspNetCore.SignalR; +using SignalRSwaggerGen.Attributes; +using Wordle.Api.Models; + +namespace Wordle.Api; +[SignalRHub] +public class WordleHub : Hub +{ + [SignalRMethod] + public async void SendMessage(string message) + { + await Clients.All.SendAsync("ReceiveMessage", message); + } +} diff --git a/wordle-web/components/Chat.vue b/wordle-web/components/Chat.vue new file mode 100644 index 00000000..2cb292c0 --- /dev/null +++ b/wordle-web/components/Chat.vue @@ -0,0 +1,47 @@ + + + diff --git a/wordle-web/components/ChatMessage.vue b/wordle-web/components/ChatMessage.vue new file mode 100644 index 00000000..dc9180d6 --- /dev/null +++ b/wordle-web/components/ChatMessage.vue @@ -0,0 +1,44 @@ + + + + diff --git a/wordle-web/package-lock.json b/wordle-web/package-lock.json index 4b2de1f0..365e0327 100644 --- a/wordle-web/package-lock.json +++ b/wordle-web/package-lock.json @@ -8,6 +8,7 @@ "hasInstallScript": true, "dependencies": { "@mdi/font": "^7.4.47", + "@microsoft/signalr": "^8.0.0", "axios": "^1.6.8", "nuxt": "^3.11.1", "nuxt-storage": "^1.2.2", @@ -1285,6 +1286,38 @@ "resolved": "https://registry.npmjs.org/@mdi/font/-/font-7.4.47.tgz", "integrity": "sha512-43MtGpd585SNzHZPcYowu/84Vz2a2g31TvPMTm9uTiCSWzaheQySUcSyUH/46fPnuPQWof2yd0pGBtzee/IQWw==" }, + "node_modules/@microsoft/signalr": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@microsoft/signalr/-/signalr-8.0.0.tgz", + "integrity": "sha512-K/wS/VmzRWePCGqGh8MU8OWbS1Zvu7DG7LSJS62fBB8rJUXwwj4axQtqrAAwKGUZHQF6CuteuQR9xMsVpM2JNA==", + "dependencies": { + "abort-controller": "^3.0.0", + "eventsource": "^2.0.2", + "fetch-cookie": "^2.0.3", + "node-fetch": "^2.6.7", + "ws": "^7.4.5" + } + }, + "node_modules/@microsoft/signalr/node_modules/ws": { + "version": "7.5.9", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.9.tgz", + "integrity": "sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==", + "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/@netlify/functions": { "version": "2.6.0", "resolved": "https://registry.npmjs.org/@netlify/functions/-/functions-2.6.0.tgz", @@ -5700,6 +5733,14 @@ "node": ">=0.8.x" } }, + "node_modules/eventsource": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-2.0.2.tgz", + "integrity": "sha512-IzUmBGPR3+oUG9dUeXynyNmf91/3zUSJg1lCktzKw47OXuhco54U3r9B7O4XX+Rb1Itm9OZ2b0RkTs10bICOxA==", + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/execa": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/execa/-/execa-7.2.0.tgz", @@ -5781,6 +5822,15 @@ "reusify": "^1.0.4" } }, + "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==", + "dependencies": { + "set-cookie-parser": "^2.4.8", + "tough-cookie": "^4.0.0" + } + }, "node_modules/file-uri-to-path": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", @@ -9029,6 +9079,24 @@ "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" }, + "node_modules/psl": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", + "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==" + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "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==" + }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -9212,6 +9280,11 @@ "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==" + }, "node_modules/resolve": { "version": "1.22.8", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", @@ -9549,6 +9622,11 @@ "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==" }, + "node_modules/set-cookie-parser": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.6.0.tgz", + "integrity": "sha512-RVnVQxTXuerk653XfuliOxBP81Sf0+qfQE73LIYKcyMYHG94AuH0kgrQpRDuTZnSmjpysHmzxJXKNfa6PjFhyQ==" + }, "node_modules/setprototypeof": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", @@ -10153,6 +10231,28 @@ "node": ">=6" } }, + "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==", + "dependencies": { + "psl": "^1.1.33", + "punycode": "^2.1.1", + "universalify": "^0.2.0", + "url-parse": "^1.5.3" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tough-cookie/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==", + "engines": { + "node": ">= 4.0.0" + } + }, "node_modules/tr46": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", @@ -10624,6 +10724,15 @@ "resolved": "https://registry.npmjs.org/uqr/-/uqr-0.1.2.tgz", "integrity": "sha512-MJu7ypHq6QasgF5YRTjqscSzQp/W11zoUk6kvmlH+fmWEs63Y0Eib13hYFwAzagRJcVY8WVnlV+eBDUGMJ5IbA==" }, + "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==", + "dependencies": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, "node_modules/urlpattern-polyfill": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/urlpattern-polyfill/-/urlpattern-polyfill-8.0.2.tgz", diff --git a/wordle-web/package.json b/wordle-web/package.json index 882e972f..0105cd5c 100644 --- a/wordle-web/package.json +++ b/wordle-web/package.json @@ -12,6 +12,7 @@ }, "dependencies": { "@mdi/font": "^7.4.47", + "@microsoft/signalr": "^8.0.0", "axios": "^1.6.8", "nuxt": "^3.11.1", "nuxt-storage": "^1.2.2", diff --git a/wordle-web/pages/index.vue b/wordle-web/pages/index.vue index 5abb8cfa..0642f564 100644 --- a/wordle-web/pages/index.vue +++ b/wordle-web/pages/index.vue @@ -1,71 +1,83 @@ @@ -78,7 +90,6 @@ const game = reactive(new Game()); game.startNewGame(); provide("GAME", game); -const myGuess = ref(""); const tokenService = new TokenService(); onMounted(() => { diff --git a/wordle-web/scripts/signalRService.ts b/wordle-web/scripts/signalRService.ts new file mode 100644 index 00000000..c117c937 --- /dev/null +++ b/wordle-web/scripts/signalRService.ts @@ -0,0 +1,59 @@ +import { HubConnection, HubConnectionBuilder } from "@microsoft/signalr"; + +export default class SignalRService { + private url: string; + private connection = ref(null); + public messages = ref([]); + + constructor() { + const baseUrl = + window.location.hostname === "localhost" || + location.hostname === "127.0.0.1" + ? "https://localhost:7266" + : "https://wordleapiewu.azurewebsites.net"; + + this.url = baseUrl + "/hub"; + this.startConnection(); + this.onReceiveMessage("ReceiveMessage", (message: string) => { + console.log(message); + console.log(this.messages.value); + this.messages.value.push(message); + }); + } + + private startConnection = async () => { + this.connection.value = new HubConnectionBuilder() + .withUrl(this.url) + .build(); + + this.connection.value.onclose((error: any) => { + console.error("Connection Closed", error); + }); + + try { + await this.connection.value.start(); + console.log("Connected!"); + } catch (error) { + console.error("Connection Error: ", error); + } + }; + + private onReceiveMessage = ( + methodName: string, + callback: (...args: any[]) => void, + ) => { + this.connection.value?.on(methodName, callback); + }; + + public sendMessage(word: string) { + if (this.connection.value) { + this.connection.value.invoke("SendMessage", word); + } + } + + public stopConnection = async () => { + if (!this.connection.value) return; + await this.connection.value.stop(); + console.log("Disconnected!"); + }; +}