From a632666203e7c6c67fdb3bcf1809a591b1d0edf1 Mon Sep 17 00:00:00 2001 From: "Emma [it/its]@Rory&" Date: Fri, 27 Dec 2024 05:48:14 +0100 Subject: Rewrite access tokens, initial admin api --- .../Spacebar.AdminAPI/Controllers/UserController.cs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 extra/admin-api/Spacebar.AdminAPI/Controllers/UserController.cs (limited to 'extra/admin-api/Spacebar.AdminAPI/Controllers/UserController.cs') diff --git a/extra/admin-api/Spacebar.AdminAPI/Controllers/UserController.cs b/extra/admin-api/Spacebar.AdminAPI/Controllers/UserController.cs new file mode 100644 index 000000000..dbe497df6 --- /dev/null +++ b/extra/admin-api/Spacebar.AdminAPI/Controllers/UserController.cs @@ -0,0 +1,17 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Spacebar.Db.Contexts; +using Spacebar.Db.Models; + +namespace Spacebar.AdminAPI.Controllers; + +[ApiController] +[Route("/users")] +public class UserController(ILogger logger, SpacebarDbContext db) : ControllerBase { + private readonly ILogger _logger = logger; + + [HttpGet(Name = "/")] + public IAsyncEnumerable Get() { + return db.Users.AsAsyncEnumerable(); + } +} \ No newline at end of file -- cgit 1.5.1 From abb1b570a4343d5e4fde34f55c1a2bf403f62981 Mon Sep 17 00:00:00 2001 From: "Emma [it/its]@Rory&" Date: Tue, 8 Apr 2025 18:55:55 +0200 Subject: Local changes --- .idea/runConfigurations/Start_API.xml | 27 ++ .idea/runConfigurations/Start_CDN.xml | 27 ++ .idea/runConfigurations/Start_Gateway.xml | 27 ++ .idea/runConfigurations/Start_bundle.xml | 27 ++ .idea/runConfigurations/Start_separated.xml | 8 + .idea/runConfigurations/start.xml | 27 -- .idea/workspace.xml | 69 ++-- README.md | 1 + .../.idea.SpacebarAdminAPI/.idea/sqldialects.xml | 7 + .../Controllers/PingController.cs | 32 ++ .../Controllers/UserController.cs | 397 ++++++++++++++++++++- .../Middleware/AuthenticationMiddleware.cs | 41 ++- extra/admin-api/Spacebar.AdminAPI/Program.cs | 12 +- .../Services/AuthenticationService.cs | 42 +++ .../Spacebar.AdminAPI/Services/Configuration.cs | 7 + .../Spacebar.AdminAPI/Spacebar.AdminAPI.csproj | 3 + .../Spacebar.AdminAPI/Spacebar.AdminAPI.http | 6 - .../Spacebar.AdminApi.Models/ApplicationModel.cs | 33 ++ .../Spacebar.AdminApi.Models/AsyncActionResult.cs | 18 + .../Spacebar.AdminApi.Models.csproj | 10 + .../Spacebar.AdminApi.Models/UserModel.cs | 57 +++ .../Spacebar.Db/Contexts/SpacebarDbContext.cs | 8 +- extra/admin-api/Spacebar.Db/Models/User.cs | 8 +- extra/admin-api/SpacebarAdminAPI.sln | 38 ++ .../Spacebar.AdminAPI.TestClient/App.razor | 12 + .../Layout/MainLayout.razor | 16 + .../Layout/MainLayout.razor.css | 77 ++++ .../Layout/NavMenu.razor | 39 ++ .../Layout/NavMenu.razor.css | 83 +++++ .../Spacebar.AdminAPI.TestClient/Pages/Home.razor | 107 ++++++ .../Spacebar.AdminAPI.TestClient/Pages/Login.razor | 57 +++ .../Spacebar.AdminAPI.TestClient/Pages/Users.razor | 72 ++++ .../Pages/UsersDelete.razor | 61 ++++ .../Spacebar.AdminAPI.TestClient/Program.cs | 59 +++ .../Properties/launchSettings.json | 15 + .../Services/Config.cs | 20 ++ .../Spacebar.AdminAPI.TestClient.csproj | 46 +++ .../Spacebar.AdminAPI.TestClient/_Imports.razor | 11 + .../wwwroot/appsettings.json | 9 + .../wwwroot/css/app.css | 114 ++++++ .../wwwroot/favicon.png | Bin 0 -> 1148 bytes .../wwwroot/icon-192.png | Bin 0 -> 2626 bytes .../wwwroot/index.html | 32 ++ .../wwwroot/lib/jetbrains-mono/jetbrains-mono.css | 118 ++++++ .../webfonts/JetBrainsMono-Bold.woff2 | Bin 0 -> 94588 bytes .../webfonts/JetBrainsMono-BoldItalic.woff2 | Bin 0 -> 98152 bytes .../webfonts/JetBrainsMono-ExtraBold.woff2 | Bin 0 -> 93692 bytes .../webfonts/JetBrainsMono-ExtraBoldItalic.woff2 | Bin 0 -> 96864 bytes .../webfonts/JetBrainsMono-ExtraLight.woff2 | Bin 0 -> 92712 bytes .../webfonts/JetBrainsMono-ExtraLightItalic.woff2 | Bin 0 -> 95816 bytes .../webfonts/JetBrainsMono-Italic.woff2 | Bin 0 -> 95864 bytes .../webfonts/JetBrainsMono-Light.woff2 | Bin 0 -> 93856 bytes .../webfonts/JetBrainsMono-LightItalic.woff2 | Bin 0 -> 97280 bytes .../webfonts/JetBrainsMono-Medium.woff2 | Bin 0 -> 93824 bytes .../webfonts/JetBrainsMono-MediumItalic.woff2 | Bin 0 -> 97808 bytes .../webfonts/JetBrainsMono-Regular.woff2 | Bin 0 -> 92164 bytes .../webfonts/JetBrainsMono-SemiBold.woff2 | Bin 0 -> 94472 bytes .../webfonts/JetBrainsMono-SemiBoldItalic.woff2 | Bin 0 -> 98124 bytes .../webfonts/JetBrainsMono-Thin.woff2 | Bin 0 -> 90612 bytes .../webfonts/JetBrainsMono-ThinItalic.woff2 | Bin 0 -> 94316 bytes .../Utilities/Spacebar.AdminAPITest/Program.cs | 12 + .../Spacebar.AdminAPITest.csproj | 18 + .../Spacebar.AdminApi.PrepareTestData/Program.cs | 24 ++ .../Spacebar.AdminApi.PrepareTestData.csproj | 16 + .../TestDataTypes/ITestData.cs | 5 + .../TestDataTypes/MessageSpamTestData.cs | 87 +++++ .../Spacebar.AdminApi.PrepareTestData/Utils.cs | 35 ++ .../RabbitMQConfiguration.cs | 12 + .../Spacebar.RabbitMqUtilities/RabbitMQService.cs | 21 ++ .../Spacebar.RabbitMqUtilities.csproj | 17 + extra/admin-api/result-man | 1 - src/api/middlewares/CORS.ts | 4 + src/gateway/opcodes/Identify.ts | 12 +- src/util/util/Token.ts | 43 ++- 74 files changed, 2095 insertions(+), 92 deletions(-) create mode 100644 .idea/runConfigurations/Start_API.xml create mode 100644 .idea/runConfigurations/Start_CDN.xml create mode 100644 .idea/runConfigurations/Start_Gateway.xml create mode 100644 .idea/runConfigurations/Start_bundle.xml create mode 100644 .idea/runConfigurations/Start_separated.xml delete mode 100644 .idea/runConfigurations/start.xml create mode 100644 extra/admin-api/.idea/.idea.SpacebarAdminAPI/.idea/sqldialects.xml create mode 100644 extra/admin-api/Spacebar.AdminAPI/Controllers/PingController.cs create mode 100644 extra/admin-api/Spacebar.AdminAPI/Services/AuthenticationService.cs create mode 100644 extra/admin-api/Spacebar.AdminAPI/Services/Configuration.cs delete mode 100644 extra/admin-api/Spacebar.AdminAPI/Spacebar.AdminAPI.http create mode 100644 extra/admin-api/Spacebar.AdminApi.Models/ApplicationModel.cs create mode 100644 extra/admin-api/Spacebar.AdminApi.Models/AsyncActionResult.cs create mode 100644 extra/admin-api/Spacebar.AdminApi.Models/Spacebar.AdminApi.Models.csproj create mode 100644 extra/admin-api/Spacebar.AdminApi.Models/UserModel.cs create mode 100644 extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/App.razor create mode 100644 extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/Layout/MainLayout.razor create mode 100644 extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/Layout/MainLayout.razor.css create mode 100644 extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/Layout/NavMenu.razor create mode 100644 extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/Layout/NavMenu.razor.css create mode 100644 extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/Pages/Home.razor create mode 100644 extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/Pages/Login.razor create mode 100644 extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/Pages/Users.razor create mode 100644 extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/Pages/UsersDelete.razor create mode 100644 extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/Program.cs create mode 100644 extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/Properties/launchSettings.json create mode 100644 extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/Services/Config.cs create mode 100644 extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/Spacebar.AdminAPI.TestClient.csproj create mode 100644 extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/_Imports.razor create mode 100644 extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/wwwroot/appsettings.json create mode 100644 extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/wwwroot/css/app.css create mode 100644 extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/wwwroot/favicon.png create mode 100644 extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/wwwroot/icon-192.png create mode 100644 extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/wwwroot/index.html create mode 100644 extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/wwwroot/lib/jetbrains-mono/jetbrains-mono.css create mode 100644 extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/wwwroot/lib/jetbrains-mono/webfonts/JetBrainsMono-Bold.woff2 create mode 100644 extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/wwwroot/lib/jetbrains-mono/webfonts/JetBrainsMono-BoldItalic.woff2 create mode 100644 extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/wwwroot/lib/jetbrains-mono/webfonts/JetBrainsMono-ExtraBold.woff2 create mode 100644 extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/wwwroot/lib/jetbrains-mono/webfonts/JetBrainsMono-ExtraBoldItalic.woff2 create mode 100644 extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/wwwroot/lib/jetbrains-mono/webfonts/JetBrainsMono-ExtraLight.woff2 create mode 100644 extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/wwwroot/lib/jetbrains-mono/webfonts/JetBrainsMono-ExtraLightItalic.woff2 create mode 100644 extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/wwwroot/lib/jetbrains-mono/webfonts/JetBrainsMono-Italic.woff2 create mode 100644 extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/wwwroot/lib/jetbrains-mono/webfonts/JetBrainsMono-Light.woff2 create mode 100644 extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/wwwroot/lib/jetbrains-mono/webfonts/JetBrainsMono-LightItalic.woff2 create mode 100644 extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/wwwroot/lib/jetbrains-mono/webfonts/JetBrainsMono-Medium.woff2 create mode 100644 extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/wwwroot/lib/jetbrains-mono/webfonts/JetBrainsMono-MediumItalic.woff2 create mode 100644 extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/wwwroot/lib/jetbrains-mono/webfonts/JetBrainsMono-Regular.woff2 create mode 100644 extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/wwwroot/lib/jetbrains-mono/webfonts/JetBrainsMono-SemiBold.woff2 create mode 100644 extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/wwwroot/lib/jetbrains-mono/webfonts/JetBrainsMono-SemiBoldItalic.woff2 create mode 100644 extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/wwwroot/lib/jetbrains-mono/webfonts/JetBrainsMono-Thin.woff2 create mode 100644 extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/wwwroot/lib/jetbrains-mono/webfonts/JetBrainsMono-ThinItalic.woff2 create mode 100644 extra/admin-api/Utilities/Spacebar.AdminAPITest/Program.cs create mode 100644 extra/admin-api/Utilities/Spacebar.AdminAPITest/Spacebar.AdminAPITest.csproj create mode 100644 extra/admin-api/Utilities/Spacebar.AdminApi.PrepareTestData/Program.cs create mode 100644 extra/admin-api/Utilities/Spacebar.AdminApi.PrepareTestData/Spacebar.AdminApi.PrepareTestData.csproj create mode 100644 extra/admin-api/Utilities/Spacebar.AdminApi.PrepareTestData/TestDataTypes/ITestData.cs create mode 100644 extra/admin-api/Utilities/Spacebar.AdminApi.PrepareTestData/TestDataTypes/MessageSpamTestData.cs create mode 100644 extra/admin-api/Utilities/Spacebar.AdminApi.PrepareTestData/Utils.cs create mode 100644 extra/admin-api/Utilities/Spacebar.RabbitMqUtilities/RabbitMQConfiguration.cs create mode 100644 extra/admin-api/Utilities/Spacebar.RabbitMqUtilities/RabbitMQService.cs create mode 100644 extra/admin-api/Utilities/Spacebar.RabbitMqUtilities/Spacebar.RabbitMqUtilities.csproj delete mode 120000 extra/admin-api/result-man (limited to 'extra/admin-api/Spacebar.AdminAPI/Controllers/UserController.cs') diff --git a/.idea/runConfigurations/Start_API.xml b/.idea/runConfigurations/Start_API.xml new file mode 100644 index 000000000..edb214cfe --- /dev/null +++ b/.idea/runConfigurations/Start_API.xml @@ -0,0 +1,27 @@ + + + + + + + + + diff --git a/extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/wwwroot/lib/jetbrains-mono/jetbrains-mono.css b/extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/wwwroot/lib/jetbrains-mono/jetbrains-mono.css new file mode 100644 index 000000000..78aedd2b1 --- /dev/null +++ b/extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/wwwroot/lib/jetbrains-mono/jetbrains-mono.css @@ -0,0 +1,118 @@ +/* source: https://gist.github.com/aasmpro/95776294ecf48bd7d0562504bad848ea */ + +/* normal fonts */ + +@font-face { + font-family: JetBrainsMono; + font-style: normal; + font-weight: 100; + src: url("./ttf/JetBrainsMono-Thin.ttf") format("truetype"); + src: url("./webfonts/JetBrainsMono-Thin.woff2") format("woff2"); +} + +@font-face { + font-family: JetBrainsMono; + font-style: normal; + font-weight: 200; + src: url("./webfonts/JetBrainsMono-ExtraLight.woff2") format("woff2"); +} + +@font-face { + font-family: JetBrainsMono; + font-style: normal; + font-weight: 300; + src: url("./webfonts/JetBrainsMono-Light.woff2") format("woff2"); +} + +@font-face { + font-family: JetBrainsMono; + font-style: normal; + font-weight: 400; + src: url("./webfonts/JetBrainsMono-Regular.woff2") format("woff2"); +} + +@font-face { + font-family: JetBrainsMono; + font-style: normal; + font-weight: 500; + src: url("./webfonts/JetBrainsMono-Medium.woff2") format("woff2"); +} + +@font-face { + font-family: JetBrainsMono; + font-style: normal; + font-weight: 600; + src: url("./webfonts/JetBrainsMono-SemiBold.woff2") format("woff2"); +} + +@font-face { + font-family: JetBrainsMono; + font-style: normal; + font-weight: 700; + src: url("./webfonts/JetBrainsMono-Bold.woff2") format("woff2"); +} + +@font-face { + font-family: JetBrainsMono; + font-style: normal; + font-weight: 800; + src: url("./webfonts/JetBrainsMono-ExtraBold.woff2") format("woff2"); +} + +/* italic fonts */ + +@font-face { + font-family: JetBrainsMono; + font-style: italic; + font-weight: 100; + src: url("./webfonts/JetBrainsMono-ThinItalic.woff2") format("woff2"); +} + +@font-face { + font-family: JetBrainsMono; + font-style: italic; + font-weight: 200; + src: url("./webfonts/JetBrainsMono-ExtraLightItalic.woff2") format("woff2"); +} + +@font-face { + font-family: JetBrainsMono; + font-style: italic; + font-weight: 300; + src: url("./webfonts/JetBrainsMono-LightItalic.woff2") format("woff2"); +} + +@font-face { + font-family: JetBrainsMono; + font-style: italic; + font-weight: 400; + src: url("./webfonts/JetBrainsMono-Italic.woff2") format("woff2"); +} + +@font-face { + font-family: JetBrainsMono; + font-style: italic; + font-weight: 500; + src: url("./webfonts/JetBrainsMono-MediumItalic.woff2") format("woff2"); +} + +@font-face { + font-family: JetBrainsMono; + font-style: italic; + font-weight: 600; + src: url("./webfonts/JetBrainsMono-SemiBoldItalic.woff2") format("woff2"); +} + +@font-face { + font-family: JetBrainsMono; + font-style: italic; + font-weight: 700; + src: url("./webfonts/JetBrainsMono-BoldItalic.woff2") format("woff2"); +} + +@font-face { + font-family: JetBrainsMono; + font-style: italic; + font-weight: 800; + src: url("./webfonts/JetBrainsMono-ExtraBoldItalic.woff2") format("woff2"); +} \ No newline at end of file diff --git a/extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/wwwroot/lib/jetbrains-mono/webfonts/JetBrainsMono-Bold.woff2 b/extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/wwwroot/lib/jetbrains-mono/webfonts/JetBrainsMono-Bold.woff2 new file mode 100644 index 000000000..4917f4341 Binary files /dev/null and b/extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/wwwroot/lib/jetbrains-mono/webfonts/JetBrainsMono-Bold.woff2 differ diff --git a/extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/wwwroot/lib/jetbrains-mono/webfonts/JetBrainsMono-BoldItalic.woff2 b/extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/wwwroot/lib/jetbrains-mono/webfonts/JetBrainsMono-BoldItalic.woff2 new file mode 100644 index 000000000..536d3f715 Binary files /dev/null and b/extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/wwwroot/lib/jetbrains-mono/webfonts/JetBrainsMono-BoldItalic.woff2 differ diff --git a/extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/wwwroot/lib/jetbrains-mono/webfonts/JetBrainsMono-ExtraBold.woff2 b/extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/wwwroot/lib/jetbrains-mono/webfonts/JetBrainsMono-ExtraBold.woff2 new file mode 100644 index 000000000..8f88c5464 Binary files /dev/null and b/extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/wwwroot/lib/jetbrains-mono/webfonts/JetBrainsMono-ExtraBold.woff2 differ diff --git a/extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/wwwroot/lib/jetbrains-mono/webfonts/JetBrainsMono-ExtraBoldItalic.woff2 b/extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/wwwroot/lib/jetbrains-mono/webfonts/JetBrainsMono-ExtraBoldItalic.woff2 new file mode 100644 index 000000000..d1478bacc Binary files /dev/null and b/extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/wwwroot/lib/jetbrains-mono/webfonts/JetBrainsMono-ExtraBoldItalic.woff2 differ diff --git a/extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/wwwroot/lib/jetbrains-mono/webfonts/JetBrainsMono-ExtraLight.woff2 b/extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/wwwroot/lib/jetbrains-mono/webfonts/JetBrainsMono-ExtraLight.woff2 new file mode 100644 index 000000000..b97239f32 Binary files /dev/null and b/extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/wwwroot/lib/jetbrains-mono/webfonts/JetBrainsMono-ExtraLight.woff2 differ diff --git a/extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/wwwroot/lib/jetbrains-mono/webfonts/JetBrainsMono-ExtraLightItalic.woff2 b/extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/wwwroot/lib/jetbrains-mono/webfonts/JetBrainsMono-ExtraLightItalic.woff2 new file mode 100644 index 000000000..be01aaca5 Binary files /dev/null and b/extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/wwwroot/lib/jetbrains-mono/webfonts/JetBrainsMono-ExtraLightItalic.woff2 differ diff --git a/extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/wwwroot/lib/jetbrains-mono/webfonts/JetBrainsMono-Italic.woff2 b/extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/wwwroot/lib/jetbrains-mono/webfonts/JetBrainsMono-Italic.woff2 new file mode 100644 index 000000000..d60c270e8 Binary files /dev/null and b/extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/wwwroot/lib/jetbrains-mono/webfonts/JetBrainsMono-Italic.woff2 differ diff --git a/extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/wwwroot/lib/jetbrains-mono/webfonts/JetBrainsMono-Light.woff2 b/extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/wwwroot/lib/jetbrains-mono/webfonts/JetBrainsMono-Light.woff2 new file mode 100644 index 000000000..653849873 Binary files /dev/null and b/extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/wwwroot/lib/jetbrains-mono/webfonts/JetBrainsMono-Light.woff2 differ diff --git a/extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/wwwroot/lib/jetbrains-mono/webfonts/JetBrainsMono-LightItalic.woff2 b/extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/wwwroot/lib/jetbrains-mono/webfonts/JetBrainsMono-LightItalic.woff2 new file mode 100644 index 000000000..66ca3d2b9 Binary files /dev/null and b/extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/wwwroot/lib/jetbrains-mono/webfonts/JetBrainsMono-LightItalic.woff2 differ diff --git a/extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/wwwroot/lib/jetbrains-mono/webfonts/JetBrainsMono-Medium.woff2 b/extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/wwwroot/lib/jetbrains-mono/webfonts/JetBrainsMono-Medium.woff2 new file mode 100644 index 000000000..669d04cdf Binary files /dev/null and b/extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/wwwroot/lib/jetbrains-mono/webfonts/JetBrainsMono-Medium.woff2 differ diff --git a/extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/wwwroot/lib/jetbrains-mono/webfonts/JetBrainsMono-MediumItalic.woff2 b/extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/wwwroot/lib/jetbrains-mono/webfonts/JetBrainsMono-MediumItalic.woff2 new file mode 100644 index 000000000..80cfd15e0 Binary files /dev/null and b/extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/wwwroot/lib/jetbrains-mono/webfonts/JetBrainsMono-MediumItalic.woff2 differ diff --git a/extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/wwwroot/lib/jetbrains-mono/webfonts/JetBrainsMono-Regular.woff2 b/extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/wwwroot/lib/jetbrains-mono/webfonts/JetBrainsMono-Regular.woff2 new file mode 100644 index 000000000..40da42765 Binary files /dev/null and b/extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/wwwroot/lib/jetbrains-mono/webfonts/JetBrainsMono-Regular.woff2 differ diff --git a/extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/wwwroot/lib/jetbrains-mono/webfonts/JetBrainsMono-SemiBold.woff2 b/extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/wwwroot/lib/jetbrains-mono/webfonts/JetBrainsMono-SemiBold.woff2 new file mode 100644 index 000000000..5ead7b0d6 Binary files /dev/null and b/extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/wwwroot/lib/jetbrains-mono/webfonts/JetBrainsMono-SemiBold.woff2 differ diff --git a/extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/wwwroot/lib/jetbrains-mono/webfonts/JetBrainsMono-SemiBoldItalic.woff2 b/extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/wwwroot/lib/jetbrains-mono/webfonts/JetBrainsMono-SemiBoldItalic.woff2 new file mode 100644 index 000000000..c5dd294b4 Binary files /dev/null and b/extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/wwwroot/lib/jetbrains-mono/webfonts/JetBrainsMono-SemiBoldItalic.woff2 differ diff --git a/extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/wwwroot/lib/jetbrains-mono/webfonts/JetBrainsMono-Thin.woff2 b/extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/wwwroot/lib/jetbrains-mono/webfonts/JetBrainsMono-Thin.woff2 new file mode 100644 index 000000000..17270e459 Binary files /dev/null and b/extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/wwwroot/lib/jetbrains-mono/webfonts/JetBrainsMono-Thin.woff2 differ diff --git a/extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/wwwroot/lib/jetbrains-mono/webfonts/JetBrainsMono-ThinItalic.woff2 b/extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/wwwroot/lib/jetbrains-mono/webfonts/JetBrainsMono-ThinItalic.woff2 new file mode 100644 index 000000000..a6432151c Binary files /dev/null and b/extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/wwwroot/lib/jetbrains-mono/webfonts/JetBrainsMono-ThinItalic.woff2 differ diff --git a/extra/admin-api/Utilities/Spacebar.AdminAPITest/Program.cs b/extra/admin-api/Utilities/Spacebar.AdminAPITest/Program.cs new file mode 100644 index 000000000..00900e0a6 --- /dev/null +++ b/extra/admin-api/Utilities/Spacebar.AdminAPITest/Program.cs @@ -0,0 +1,12 @@ +// See https://aka.ms/new-console-template for more information + +using System.Net.Http.Json; +using ArcaneLibs.Extensions; + +Console.WriteLine("Hello, World!"); +using var hc = new HttpClient(); +var response = hc.GetFromJsonAsAsyncEnumerable("http://localhost:5112/users/1183568750931099679/deactivate"); +await foreach (var item in response) { + Console.WriteLine(item.ToJson(indent: false)); +} + \ No newline at end of file diff --git a/extra/admin-api/Utilities/Spacebar.AdminAPITest/Spacebar.AdminAPITest.csproj b/extra/admin-api/Utilities/Spacebar.AdminAPITest/Spacebar.AdminAPITest.csproj new file mode 100644 index 000000000..d9a0370ab --- /dev/null +++ b/extra/admin-api/Utilities/Spacebar.AdminAPITest/Spacebar.AdminAPITest.csproj @@ -0,0 +1,18 @@ + + + + Exe + net9.0 + preview + enable + enable + true + + + + + ..\..\..\..\..\..\..\.nuget\packages\arcanelibs\1.0.0-preview.20241210-161342\lib\net9.0\ArcaneLibs.dll + + + + diff --git a/extra/admin-api/Utilities/Spacebar.AdminApi.PrepareTestData/Program.cs b/extra/admin-api/Utilities/Spacebar.AdminApi.PrepareTestData/Program.cs new file mode 100644 index 000000000..4bfb037a9 --- /dev/null +++ b/extra/admin-api/Utilities/Spacebar.AdminApi.PrepareTestData/Program.cs @@ -0,0 +1,24 @@ +// See https://aka.ms/new-console-template for more information + +using ArcaneLibs; +using Spacebar.AdminApi.PrepareTestData.TestDataTypes; + +Console.WriteLine("Hello, World!"); +var tests = ClassCollector.ResolveFromAllAccessibleAssemblies(); +foreach (var test in tests) { + Console.WriteLine(test.Name); +} + +Console.Write("Enter test type to run: "); +var testType = Console.ReadLine(); +var testToRun = tests.FirstOrDefault(t => t.Name == testType); +var runMethod = testToRun?.GetMethod("Run"); + +if (runMethod != null) { + Console.WriteLine($"Running test {testToRun.FullName}..."); + var task = runMethod.Invoke(testToRun, null) as Task; + await task!; + Console.WriteLine($"Test {testToRun.FullName} completed."); +} else { + Console.WriteLine("Test not found."); +} \ No newline at end of file diff --git a/extra/admin-api/Utilities/Spacebar.AdminApi.PrepareTestData/Spacebar.AdminApi.PrepareTestData.csproj b/extra/admin-api/Utilities/Spacebar.AdminApi.PrepareTestData/Spacebar.AdminApi.PrepareTestData.csproj new file mode 100644 index 000000000..de3115ae2 --- /dev/null +++ b/extra/admin-api/Utilities/Spacebar.AdminApi.PrepareTestData/Spacebar.AdminApi.PrepareTestData.csproj @@ -0,0 +1,16 @@ + + + + Exe + net9.0 + preview + enable + enable + true + + + + + + + diff --git a/extra/admin-api/Utilities/Spacebar.AdminApi.PrepareTestData/TestDataTypes/ITestData.cs b/extra/admin-api/Utilities/Spacebar.AdminApi.PrepareTestData/TestDataTypes/ITestData.cs new file mode 100644 index 000000000..4c667c65e --- /dev/null +++ b/extra/admin-api/Utilities/Spacebar.AdminApi.PrepareTestData/TestDataTypes/ITestData.cs @@ -0,0 +1,5 @@ +namespace Spacebar.AdminApi.PrepareTestData.TestDataTypes; + +public interface ITestData { + +} \ No newline at end of file diff --git a/extra/admin-api/Utilities/Spacebar.AdminApi.PrepareTestData/TestDataTypes/MessageSpamTestData.cs b/extra/admin-api/Utilities/Spacebar.AdminApi.PrepareTestData/TestDataTypes/MessageSpamTestData.cs new file mode 100644 index 000000000..5ac792ad7 --- /dev/null +++ b/extra/admin-api/Utilities/Spacebar.AdminApi.PrepareTestData/TestDataTypes/MessageSpamTestData.cs @@ -0,0 +1,87 @@ +using System.Net.Http.Json; +using System.Text.Json.Nodes; + +namespace Spacebar.AdminApi.PrepareTestData.TestDataTypes; + +public class MessageSpamTestData : ITestData { + public static async Task Run() { + using var hc = new HttpClient(); + var token = await Utils.CreateUser(); + hc.DefaultRequestHeaders.Authorization = new("Bearer", token); + + int guildCount = 1; + int channelCount = 100; // per guild + int messageCount = 1000; // per channel + + for (int guild = 0; guild < guildCount; guild++) { + var guildId = await Utils.CreateGuild(token); + // for (int channel = 0; channel < channelCount; channel++) { + // Console.WriteLine($"> Creating channel {channel} in guild {guild}..."); + // var channelRequest = await hc.PostAsJsonAsync($"http://localhost:3001/api/v9/guilds/{guildId}/channels", new { + // name = Guid.NewGuid().ToString()[0..30], + // type = 0 + // }); + // var channelResponse = await channelRequest.Content.ReadFromJsonAsync(); + // var channelId = channelResponse!["id"]!.ToString(); + // await SendMessages(hc, channelId, messageCount); + // } + + var ss = new SemaphoreSlim(16,16); + var tasks = Enumerable.Range(0, channelCount).Select(async channel => { + await ss.WaitAsync(); + Console.WriteLine($"> Creating channel {channel} in guild {guildId}..."); + var channelRequest = await hc.PostAsJsonAsync($"http://localhost:3001/api/v9/guilds/{guildId}/channels", new { + name = Guid.NewGuid().ToString()[0..30], + type = 0 + }); + var channelResponse = await channelRequest.Content.ReadFromJsonAsync(); + var channelId = channelResponse!["id"]!.ToString(); + await SendMessages(hc, channelId, messageCount); + ss.Release(); + }); + await Task.WhenAll(tasks); + } + } + + private static async Task CreateChannels(HttpClient hc, string guildId, int channelCount) { + var tasks = Enumerable.Range(0, channelCount).Select(async channel => { + Console.WriteLine($"> Creating channel {channel} in guild {guildId}..."); + var channelRequest = await hc.PostAsJsonAsync($"http://localhost:3001/api/v9/guilds/{guildId}/channels", new { + name = Guid.NewGuid().ToString()[0..30], + type = 0 + }); + var channelResponse = await channelRequest.Content.ReadFromJsonAsync(); + }); + await Task.WhenAll(tasks); + } + + private static async Task SendMessages(HttpClient hc, string channelId, int maxMessageCount) { + // var ss = new SemaphoreSlim(32, 32); + // var tasks = Enumerable.Range(0, Random.Shared.Next((int)(0.75 * maxMessageCount), maxMessageCount)).Select(async message => { + // var success = false; + // while (!success) { + // await ss.WaitAsync(); + // Console.WriteLine($"> Sending message {message} in channel {channelId}..."); + // var messageReq = await hc.PostAsJsonAsync($"http://localhost:3001/api/v9/channels/{channelId}/messages", new { + // content = Guid.NewGuid().ToString() + // }); + // var messageResponse = await messageReq.Content.ReadFromJsonAsync(); + // if (messageResponse.ContainsKey("id")) { + // success = true; + // Console.WriteLine(messageResponse!["id"]!.ToString()); + // } + // } + // + // ss.Release(); + // }); + // await Task.WhenAll(tasks); + + var messageReq = await hc.PostAsJsonAsync($"http://localhost:3001/api/v9/channels/{channelId}/messages", new { + content = Guid.NewGuid().ToString() + }); + var messageResponse = await messageReq.Content.ReadFromJsonAsync(); + if (messageResponse.ContainsKey("id")) { + await hc.GetAsync($"http://localhost:5112/Users/duplicate/{messageResponse!["id"]!.ToString()}?count={maxMessageCount}"); + } + } +} \ No newline at end of file diff --git a/extra/admin-api/Utilities/Spacebar.AdminApi.PrepareTestData/Utils.cs b/extra/admin-api/Utilities/Spacebar.AdminApi.PrepareTestData/Utils.cs new file mode 100644 index 000000000..143f50a44 --- /dev/null +++ b/extra/admin-api/Utilities/Spacebar.AdminApi.PrepareTestData/Utils.cs @@ -0,0 +1,35 @@ +using System.Net.Http.Json; +using System.Text.Json.Nodes; +using ArcaneLibs.Extensions; + +namespace Spacebar.AdminApi.PrepareTestData; + +public static class Utils { + public static async Task CreateUser() { + Console.WriteLine("> Creating user..."); + using var hc = new HttpClient(); + var registerRequest = await hc.PostAsJsonAsync("http://localhost:3001/api/v9/auth/register", new { + username = Guid.NewGuid().ToString()[0..30], + password = "password", + email = $"{Guid.NewGuid()}@example.com", + consent = true, + date_of_birth = "2000-01-01", + }); + var registerResponse = await registerRequest.Content.ReadFromJsonAsync(); + return registerResponse!["token"]!.ToString(); + } + + public static async Task CreateGuild(string token) { + using var hc = new HttpClient(); + hc.DefaultRequestHeaders.Authorization = new("Bearer", token); + + Console.WriteLine("> Creating guild..."); + var guildRequest = await hc.PostAsJsonAsync("http://localhost:3001/api/v9/guilds", new { + name = Guid.NewGuid().ToString()[0..30] + }); + + var guildResponse = await guildRequest.Content.ReadFromJsonAsync(); + var guildId = guildResponse!["id"]!.ToString(); + return guildId; + } +} \ No newline at end of file diff --git a/extra/admin-api/Utilities/Spacebar.RabbitMqUtilities/RabbitMQConfiguration.cs b/extra/admin-api/Utilities/Spacebar.RabbitMqUtilities/RabbitMQConfiguration.cs new file mode 100644 index 000000000..e31e8c365 --- /dev/null +++ b/extra/admin-api/Utilities/Spacebar.RabbitMqUtilities/RabbitMQConfiguration.cs @@ -0,0 +1,12 @@ +using Microsoft.Extensions.Configuration; + +namespace Spacebar.RabbitMqUtilities; + +public class RabbitMQConfiguration { + public RabbitMQConfiguration(IConfiguration configuration) { + configuration.GetRequiredSection("RabbitMQ").Bind(this); + } + public required string Host { get; set; } + public required string Username { get; set; } + public required string Password { get; set; } +} \ No newline at end of file diff --git a/extra/admin-api/Utilities/Spacebar.RabbitMqUtilities/RabbitMQService.cs b/extra/admin-api/Utilities/Spacebar.RabbitMqUtilities/RabbitMQService.cs new file mode 100644 index 000000000..222559429 --- /dev/null +++ b/extra/admin-api/Utilities/Spacebar.RabbitMqUtilities/RabbitMQService.cs @@ -0,0 +1,21 @@ +using RabbitMQ.Client; + +namespace Spacebar.RabbitMqUtilities; + +public interface IRabbitMQService { + IConnection CreateChannel(); +} + +public class RabbitMQService(RabbitMQConfiguration config) : IRabbitMQService { + public IConnection CreateChannel() { + var connection = new ConnectionFactory { + UserName = config.Username, + Password = config.Password, + HostName = config.Host, + // DispatchConsumersAsync = true + }; + + var channel = connection.CreateConnection(); + return channel; + } +} \ No newline at end of file diff --git a/extra/admin-api/Utilities/Spacebar.RabbitMqUtilities/Spacebar.RabbitMqUtilities.csproj b/extra/admin-api/Utilities/Spacebar.RabbitMqUtilities/Spacebar.RabbitMqUtilities.csproj new file mode 100644 index 000000000..be1f4a773 --- /dev/null +++ b/extra/admin-api/Utilities/Spacebar.RabbitMqUtilities/Spacebar.RabbitMqUtilities.csproj @@ -0,0 +1,17 @@ + + + + net9.0 + preview + enable + enable + + + + + + + + + + diff --git a/extra/admin-api/result-man b/extra/admin-api/result-man deleted file mode 120000 index 0f21db9c6..000000000 --- a/extra/admin-api/result-man +++ /dev/null @@ -1 +0,0 @@ -/nix/store/0q9yki1d9czy7i7mly8gy3ffjvc3hkqv-postgresql-16.5-man \ No newline at end of file diff --git a/src/api/middlewares/CORS.ts b/src/api/middlewares/CORS.ts index 3e7452fc0..ca6abb82d 100644 --- a/src/api/middlewares/CORS.ts +++ b/src/api/middlewares/CORS.ts @@ -36,5 +36,9 @@ export function CORS(req: Request, res: Response, next: NextFunction) { req.header("Access-Control-Request-Methods") || "*", ); + if (req.method === "OPTIONS") { + res.status(204).end(); + return; + } next(); } diff --git a/src/gateway/opcodes/Identify.ts b/src/gateway/opcodes/Identify.ts index 3f3164fca..09e8805fa 100644 --- a/src/gateway/opcodes/Identify.ts +++ b/src/gateway/opcodes/Identify.ts @@ -64,10 +64,17 @@ import { PreloadedUserSettings } from "discord-protos"; // TODO: user sharding // TODO: check privileged intents, if defined in the config +function logAuth(message: string) { + if (process.env.LOG_AUTH != "true") return; + console.log(`[Gateway/Auth] ${message}`); +} + const tryGetUserFromToken = async (...args: Parameters) => { + logAuth("Checking token"); try { return (await checkToken(...args)).user; } catch (e) { + console.log("[Gateway] Error when identifying: ", e); return null; } }; @@ -96,7 +103,10 @@ export async function onIdentify(this: WebSocket, data: Payload) { relations: ["relationships", "relationships.to", "settings"], select: [...PrivateUserProjection, "relationships"], }); - if (!user) return this.close(CLOSECODES.Authentication_failed); + if (!user) { + console.log("[Gateway] Failed to identify user"); + return this.close(CLOSECODES.Authentication_failed); + } this.user_id = user.id; const userQueryTime = taskSw.getElapsedAndReset(); diff --git a/src/util/util/Token.ts b/src/util/util/Token.ts index 844d21acd..3de05aa2a 100644 --- a/src/util/util/Token.ts +++ b/src/util/util/Token.ts @@ -36,6 +36,16 @@ export type UserTokenData = { decoded: { id: string; iat: number }; }; +function logAuth(text: string) { + if(process.env.LOG_AUTH !== "true") return; + console.log(`[AUTH] ${text}`); +} + +function rejectAndLog(rejectFunction: (reason?: any) => void, reason: any) { + console.error(reason); + rejectFunction(reason); +} + export const checkToken = ( token: string, opts?: { @@ -49,12 +59,16 @@ export const checkToken = ( const validateUser: jwt.VerifyCallback = async (err, out) => { const decoded = out as UserTokenData["decoded"]; - if (err || !decoded) return reject("Invalid Token meow " + err); + if (err || !decoded) { + logAuth("validateUser rejected: " + err); + return rejectAndLog(reject, "Invalid Token meow " + err); + } const user = await User.findOne({ where: { id: decoded.id }, select: [ ...(opts?.select || []), + "id", "bot", "disabled", "deleted", @@ -64,23 +78,36 @@ export const checkToken = ( relations: opts?.relations, }); - if (!user) return reject("User not found"); + if (!user) { + logAuth("validateUser rejected: User not found"); + return rejectAndLog(reject, "User not found"); + } // we need to round it to seconds as it saved as seconds in jwt iat and valid_tokens_since is stored in milliseconds if ( decoded.iat * 1000 < new Date(user.data.valid_tokens_since).setSeconds(0, 0) - ) - return reject("Invalid Token"); - - if (user.disabled) return reject("User disabled"); - if (user.deleted) return reject("User not found"); - + ) { + logAuth("validateUser rejected: Token not yet valid"); + return rejectAndLog(reject, "Invalid Token"); + } + + if (user.disabled) { + logAuth("validateUser rejected: User disabled"); + return rejectAndLog(reject, "User disabled"); + } + if (user.deleted) { + logAuth("validateUser rejected: User deleted"); + return rejectAndLog(reject, "User not found"); + } + + logAuth("validateUser success: " + JSON.stringify({ decoded, user })); return resolve({ decoded, user }); }; const dec = jwt.decode(token, { complete: true }); if (!dec) return reject("Could not parse token"); + logAuth("Decoded token: " + JSON.stringify(dec)); if (dec.header.alg == "HS256") { jwt.verify( -- cgit 1.5.1 From e1483e9f90ef506444a75499768ec0d7c27bfdc8 Mon Sep 17 00:00:00 2001 From: "Emma [it/its]@Rory&" Date: Thu, 10 Apr 2025 17:12:53 +0200 Subject: Further admin api work --- .../Controllers/Media/UserMediaController.cs | 27 ++ .../Controllers/UserController.cs | 7 + .../Middleware/AuthenticationMiddleware.cs | 4 +- .../Services/AuthenticationService.cs | 4 +- .../Spacebar.AdminAPI/Services/Configuration.cs | 2 + .../Spacebar.AdminApi.Models/FileMetadataModel.cs | 23 ++ extra/admin-api/Spacebar.Db/Models/User.cs | 2 +- .../Layout/NavMenu.razor | 5 + .../Pages/Media/Index.razor | 7 + .../Pages/Media/Users.razor | 106 ++++++++ .../Spacebar.AdminAPI.TestClient/Pages/Users.razor | 24 +- .../Pages/UsersDelete.razor | 13 +- .../Services/StreamingHttpClient.cs | 296 +++++++++++++++++++++ .../Spacebar.AdminApi.PrepareTestData/Constants.cs | 7 + .../Spacebar.AdminApi.PrepareTestData/Program.cs | 12 +- .../TestDataTypes/AttachmentSpamTestData.cs | 87 ++++++ .../Spacebar.AdminApi.PrepareTestData/Utils.cs | 36 ++- 17 files changed, 642 insertions(+), 20 deletions(-) create mode 100644 extra/admin-api/Spacebar.AdminAPI/Controllers/Media/UserMediaController.cs create mode 100644 extra/admin-api/Spacebar.AdminApi.Models/FileMetadataModel.cs create mode 100644 extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/Pages/Media/Index.razor create mode 100644 extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/Pages/Media/Users.razor create mode 100644 extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/Services/StreamingHttpClient.cs create mode 100644 extra/admin-api/Utilities/Spacebar.AdminApi.PrepareTestData/Constants.cs create mode 100644 extra/admin-api/Utilities/Spacebar.AdminApi.PrepareTestData/TestDataTypes/AttachmentSpamTestData.cs (limited to 'extra/admin-api/Spacebar.AdminAPI/Controllers/UserController.cs') diff --git a/extra/admin-api/Spacebar.AdminAPI/Controllers/Media/UserMediaController.cs b/extra/admin-api/Spacebar.AdminAPI/Controllers/Media/UserMediaController.cs new file mode 100644 index 000000000..a06d110bb --- /dev/null +++ b/extra/admin-api/Spacebar.AdminAPI/Controllers/Media/UserMediaController.cs @@ -0,0 +1,27 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Spacebar.AdminApi.Models; +using Spacebar.Db.Contexts; +using Spacebar.Db.Models; +using Spacebar.RabbitMqUtilities; + +namespace Spacebar.AdminAPI.Controllers.Media; + +[ApiController] +[Route("/media/user")] +public class UserMediaController(ILogger logger, SpacebarDbContext db, RabbitMQService mq, IServiceProvider sp) : ControllerBase { + + [HttpGet("{userId}/attachments")] + public async IAsyncEnumerable GetAttachmentsByUser(string userId) { + var db2 = sp.CreateScope().ServiceProvider.GetService(); + var attachments = db.Attachments + // .IgnoreAutoIncludes() + .Where(x => x.Message!.AuthorId == userId) + .AsAsyncEnumerable(); + await foreach (var attachment in attachments) { + attachment.Message = await db2.Messages.FindAsync(attachment.MessageId); + // attachment.Message.Author = await db2.Users.FindAsync(attachment.Message.AuthorId); + yield return attachment; + } + } +} \ No newline at end of file diff --git a/extra/admin-api/Spacebar.AdminAPI/Controllers/UserController.cs b/extra/admin-api/Spacebar.AdminAPI/Controllers/UserController.cs index 3b753ebde..620382e1f 100644 --- a/extra/admin-api/Spacebar.AdminAPI/Controllers/UserController.cs +++ b/extra/admin-api/Spacebar.AdminAPI/Controllers/UserController.cs @@ -146,6 +146,13 @@ public class UserController(ILogger logger, SpacebarDbContext db yield break; } + user.Data = "{}"; + user.Deleted = true; + user.Disabled = true; + user.Rights = 0; + db.Users.Update(user); + await db.SaveChangesAsync(); + var factory = new ConnectionFactory { Uri = new Uri("amqp://guest:guest@127.0.0.1/") }; diff --git a/extra/admin-api/Spacebar.AdminAPI/Middleware/AuthenticationMiddleware.cs b/extra/admin-api/Spacebar.AdminAPI/Middleware/AuthenticationMiddleware.cs index c87d568fe..2dc4c9774 100644 --- a/extra/admin-api/Spacebar.AdminAPI/Middleware/AuthenticationMiddleware.cs +++ b/extra/admin-api/Spacebar.AdminAPI/Middleware/AuthenticationMiddleware.cs @@ -2,6 +2,7 @@ using System.IdentityModel.Tokens.Jwt; using System.Security.Cryptography; using ArcaneLibs.Extensions; using Microsoft.IdentityModel.Tokens; +using Spacebar.AdminAPI.Services; using Spacebar.Db.Contexts; using Spacebar.Db.Models; @@ -54,7 +55,8 @@ public class AuthenticationMiddleware(RequestDelegate next) { if (!_userCache.ContainsKey(token)) { var db = sp.GetRequiredService(); - user = await db.Users.FindAsync(res.ClaimsIdentity.Claims.First(x => x.Type == "id").Value) + var config = sp.GetRequiredService(); + user = await db.Users.FindAsync(config.OverrideUid ?? res.ClaimsIdentity.Claims.First(x => x.Type == "id").Value) ?? throw new InvalidOperationException(); _userCache[token] = user; _userCacheExpiry[token] = DateTime.Now.AddMinutes(5); diff --git a/extra/admin-api/Spacebar.AdminAPI/Services/AuthenticationService.cs b/extra/admin-api/Spacebar.AdminAPI/Services/AuthenticationService.cs index 2ec7fab15..53d2cf217 100644 --- a/extra/admin-api/Spacebar.AdminAPI/Services/AuthenticationService.cs +++ b/extra/admin-api/Spacebar.AdminAPI/Services/AuthenticationService.cs @@ -7,7 +7,7 @@ using Spacebar.Db.Models; namespace Spacebar.AdminAPI.Services; -public class AuthenticationService(SpacebarDbContext db) { +public class AuthenticationService(SpacebarDbContext db, Configuration config) { private static Dictionary _userCache = new(); private static Dictionary _userCacheExpiry = new(); @@ -37,6 +37,6 @@ public class AuthenticationService(SpacebarDbContext db) { throw new UnauthorizedAccessException(); } - return await db.Users.FindAsync(res.ClaimsIdentity.Claims.First(x => x.Type == "id").Value) ?? throw new InvalidOperationException(); + return await db.Users.FindAsync(config.OverrideUid ?? res.ClaimsIdentity.Claims.First(x => x.Type == "id").Value) ?? throw new InvalidOperationException(); } } \ No newline at end of file diff --git a/extra/admin-api/Spacebar.AdminAPI/Services/Configuration.cs b/extra/admin-api/Spacebar.AdminAPI/Services/Configuration.cs index a580e6c64..a988a316e 100644 --- a/extra/admin-api/Spacebar.AdminAPI/Services/Configuration.cs +++ b/extra/admin-api/Spacebar.AdminAPI/Services/Configuration.cs @@ -4,4 +4,6 @@ public class Configuration { public Configuration(IConfiguration configuration) { configuration.GetRequiredSection("SpacebarAdminApi").Bind(this); } + + public string? OverrideUid { get; set; } } \ No newline at end of file diff --git a/extra/admin-api/Spacebar.AdminApi.Models/FileMetadataModel.cs b/extra/admin-api/Spacebar.AdminApi.Models/FileMetadataModel.cs new file mode 100644 index 000000000..dfc577a90 --- /dev/null +++ b/extra/admin-api/Spacebar.AdminApi.Models/FileMetadataModel.cs @@ -0,0 +1,23 @@ +using System.Text.Json.Serialization; + +namespace Spacebar.AdminApi.Models; + +public class FileMetadataModel { + public string UserId { get; set; } = null!; + public string Id { get; set; } = null!; + + [JsonConverter(typeof(JsonStringEnumConverter))] + public FileUploadType Type { get; set; } + + + public enum FileUploadType { + Attachment, + Avatar, + Banner, + GuildIcon, + GuildSplash, + GuildCover, + Emoji, + Sticker + } +} \ No newline at end of file diff --git a/extra/admin-api/Spacebar.Db/Models/User.cs b/extra/admin-api/Spacebar.Db/Models/User.cs index 114572fb7..94fc5fa6a 100644 --- a/extra/admin-api/Spacebar.Db/Models/User.cs +++ b/extra/admin-api/Spacebar.Db/Models/User.cs @@ -92,7 +92,7 @@ public partial class User [Column("email", TypeName = "character varying")] public string? Email { get; set; } - [Column("flags")] + [Column("flags", TypeName = "text")] public ulong Flags { get; set; } [Column("public_flags")] diff --git a/extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/Layout/NavMenu.razor b/extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/Layout/NavMenu.razor index 8a96bc597..732d46e79 100644 --- a/extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/Layout/NavMenu.razor +++ b/extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/Layout/NavMenu.razor @@ -24,6 +24,11 @@ Guilds + diff --git a/extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/Pages/Media/Index.razor b/extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/Pages/Media/Index.razor new file mode 100644 index 000000000..76621e74c --- /dev/null +++ b/extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/Pages/Media/Index.razor @@ -0,0 +1,7 @@ +@page "/Media" +

Index of /Media

+
+ +@code { + +} \ No newline at end of file diff --git a/extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/Pages/Media/Users.razor b/extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/Pages/Media/Users.razor new file mode 100644 index 000000000..81008a258 --- /dev/null +++ b/extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/Pages/Media/Users.razor @@ -0,0 +1,106 @@ +@page "/Media/ByUser" +@using System.Net.Http.Headers +@using System.Reflection +@using Spacebar.AdminApi.Models +@using Spacebar.AdminAPI.TestClient.Services +@using ArcaneLibs.Blazor.Components +@inject Config Config +@inject ILocalStorageService LocalStorage + +Uploaded media by user + +
+ Displayed columns + @foreach (var column in DisplayedColumns) { + var value = column.Value; + + + @column.Key.Name + +
+ } +
+ + + + @if (UserList is { Count: > 0 }) { + @foreach (var user in UserList.OrderByDescending(u => u.Id).Where(x => !x.Deleted)) { + + } + } + + + + + @{ + var columns = DisplayedColumns.Where(kvp => kvp.Value).Select(kvp => kvp.Key).ToList(); + } + + + @foreach (var column in columns) { + + } + + + + + @foreach (var user in UserMedia) { + + @foreach (var column in columns) { + + } + + + } + +
@column.NameActions
@column.GetValue(user) + Delete +
+ +@code { + + private Dictionary DisplayedColumns { get; set; } = typeof(FileMetadataModel).GetProperties() + .ToDictionary(p => p, p => p.Name == "Username" || p.Name == "Id" || p.Name == "MessageCount"); + + private List UserList { get; set; } = new(); + private List UserMedia { get; set; } = new(); + + [SupplyParameterFromQuery(Name = "UserId")] + public string? SelectedUserId { + get; + set { + field = value; + if (string.IsNullOrWhiteSpace(field)) + UserMedia.Clear(); + else _ = GetMediaForUser(value!); + } + } + + protected override async Task OnInitializedAsync() { + using var hc = new HttpClient(); + hc.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", Config.AccessToken); + var response = await hc.GetAsync(Config.AdminUrl + "/_spacebar/admin/users/"); + if (!response.IsSuccessStatusCode) throw new Exception(await response.Content.ReadAsStringAsync()); + var content = response.Content.ReadFromJsonAsAsyncEnumerable(); + await foreach (var user in content) { + UserList.Add(user!); + StateHasChanged(); + } + } + + private async Task GetMediaForUser(string userId) { + using var hc = new HttpClient(); + hc.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", Config.AccessToken); + var response = await hc.GetAsync(Config.AdminUrl + $"/_spacebar/admin/media/user/{userId}/attachments"); + if (!response.IsSuccessStatusCode) throw new Exception(await response.Content.ReadAsStringAsync()); + var content = response.Content.ReadFromJsonAsAsyncEnumerable(); + await foreach (var media in content) { + UserMedia.Add(media!); + StateHasChanged(); + } + } + +} \ No newline at end of file diff --git a/extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/Pages/Users.razor b/extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/Pages/Users.razor index 13c346617..c0e678678 100644 --- a/extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/Pages/Users.razor +++ b/extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/Pages/Users.razor @@ -4,6 +4,7 @@ @using Spacebar.AdminApi.Models @using Spacebar.AdminAPI.TestClient.Services @using ArcaneLibs.Blazor.Components +@using ArcaneLibs.Extensions @inject Config Config @inject ILocalStorageService LocalStorage @@ -24,6 +25,7 @@ } +

Got @UserList.Count users.

@{ var columns = DisplayedColumns.Where(kvp => kvp.Value).Select(kvp => kvp.Key).ToList(); @@ -37,7 +39,7 @@ - @foreach (var user in UserList) { + @foreach (var user in UserList.Where(x => !x.Deleted).OrderByDescending(x=>x.MessageCount)) { @foreach (var column in columns) { @@ -58,15 +60,21 @@ private List UserList { get; set; } = new(); protected override async Task OnInitializedAsync() { - using var hc = new HttpClient(); + var hc = new StreamingHttpClient(); hc.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", Config.AccessToken); - var response = await hc.GetAsync(Config.AdminUrl + "/_spacebar/admin/users/"); - if (!response.IsSuccessStatusCode) throw new Exception(await response.Content.ReadAsStringAsync()); - var content = response.Content.ReadFromJsonAsAsyncEnumerable(); - await foreach (var user in content) { - UserList.Add(user); - StateHasChanged(); + + // var request = new HttpRequestMessage(HttpMethod.Get, Config.AdminUrl + "/_spacebar/admin/users/"); + + var response = hc.GetAsyncEnumerableFromJsonAsync(Config.AdminUrl + "/_spacebar/admin/users/"); + // if (!response.IsSuccessStatusCode) throw new Exception(await response.Content.ReadAsStringAsync()); + // var content = response.Content.ReadFromJsonAsAsyncEnumerable(); + await foreach (var user in response) { + // Console.WriteLine(user.ToJson(indent: false, ignoreNull: true)); + UserList.Add(user!); + if(UserList.Count % 1000 == 0) + StateHasChanged(); } + StateHasChanged(); } } \ No newline at end of file diff --git a/extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/Pages/UsersDelete.razor b/extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/Pages/UsersDelete.razor index 084a372fe..98a3e0fc7 100644 --- a/extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/Pages/UsersDelete.razor +++ b/extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/Pages/UsersDelete.razor @@ -14,15 +14,21 @@ Deleted @ChannelDeleteProgress.Sum(x=>x.Value.Deleted) messages so far! } +@if (Done) { +

Done!

+} + @code { [Parameter] public required string Id { get; set; } private Dictionary ChannelDeleteProgress { get; set; } = new(); + + private bool Done { get; set; } protected override async Task OnInitializedAsync() { - using var hc = new HttpClient(); + var hc = new StreamingHttpClient(); hc.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", Config.AccessToken); var response = await hc.GetAsync(Config.AdminUrl + $"/_spacebar/admin/Users/{Id}/delete?messageDeleteChunkSize=100"); if (!response.IsSuccessStatusCode) throw new Exception(await response.Content.ReadAsStringAsync()); @@ -47,10 +53,13 @@ Deleted @ChannelDeleteProgress.Sum(x=>x.Value.Deleted) messages so far! break; } } - + StateHasChanged(); await Task.Delay(1); } + + Done = true; + StateHasChanged(); } private class DeleteProgress { diff --git a/extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/Services/StreamingHttpClient.cs b/extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/Services/StreamingHttpClient.cs new file mode 100644 index 000000000..67dc673e3 --- /dev/null +++ b/extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/Services/StreamingHttpClient.cs @@ -0,0 +1,296 @@ +#define SINGLE_HTTPCLIENT // Use a single HttpClient instance for all MatrixHttpClient instances +// #define SYNC_HTTPCLIENT // Only allow one request as a time, for debugging +using System.Data; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Net.Http.Headers; +using System.Reflection; +using System.Text; +using System.Text.Json; +using System.Text.Json.Serialization; +using ArcaneLibs; +using ArcaneLibs.Extensions; + +namespace Spacebar.AdminAPI.TestClient.Services; + +#if SINGLE_HTTPCLIENT +// TODO: Add URI wrapper for +public class StreamingHttpClient { + private static readonly HttpClient Client; + + static StreamingHttpClient() { + try { + var handler = new SocketsHttpHandler { + PooledConnectionLifetime = TimeSpan.FromMinutes(15), + MaxConnectionsPerServer = 4096, + EnableMultipleHttp2Connections = true + }; + Client = new HttpClient(handler) { + DefaultRequestVersion = new Version(3, 0), + Timeout = TimeSpan.FromDays(1) + }; + } + catch (PlatformNotSupportedException e) { + Console.WriteLine("Failed to create HttpClient with connection pooling, continuing without connection pool!"); + Console.WriteLine("Original exception (safe to ignore!):"); + Console.WriteLine(e); + + Client = new HttpClient { + DefaultRequestVersion = new Version(3, 0) + }; + } + catch (Exception e) { + Console.WriteLine("Failed to create HttpClient:"); + Console.WriteLine(e); + throw; + } + } + +#if SYNC_HTTPCLIENT + internal SemaphoreSlim _rateLimitSemaphore { get; } = new(1, 1); +#endif + + public static bool LogRequests = true; + public Dictionary AdditionalQueryParameters { get; set; } = new(); + + public Uri? BaseAddress { get; set; } + + // default headers, not bound to client + public HttpRequestHeaders DefaultRequestHeaders { get; set; } = + typeof(HttpRequestHeaders).GetConstructor(BindingFlags.NonPublic | BindingFlags.Instance, null, [], null)?.Invoke([]) as HttpRequestHeaders ?? + throw new InvalidOperationException("Failed to create HttpRequestHeaders"); + + private static JsonSerializerOptions GetJsonSerializerOptions(JsonSerializerOptions? options = null) { + options ??= new JsonSerializerOptions(); + // options.Converters.Add(new JsonFloatStringConverter()); + // options.Converters.Add(new JsonDoubleStringConverter()); + // options.Converters.Add(new JsonDecimalStringConverter()); + options.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull; + return options; + } + + public async Task SendUnhandledAsync(HttpRequestMessage request, CancellationToken cancellationToken) { + if (request.RequestUri is null) throw new NullReferenceException("RequestUri is null"); + // if (!request.RequestUri.IsAbsoluteUri) + request.RequestUri = request.RequestUri.EnsureAbsolute(BaseAddress!); + var swWait = Stopwatch.StartNew(); +#if SYNC_HTTPCLIENT + await _rateLimitSemaphore.WaitAsync(cancellationToken); +#endif + + if (request.RequestUri is null) throw new NullReferenceException("RequestUri is null"); + if (!request.RequestUri.IsAbsoluteUri) + request.RequestUri = new Uri(BaseAddress ?? throw new InvalidOperationException("Relative URI passed, but no BaseAddress is specified!"), request.RequestUri); + swWait.Stop(); + var swExec = Stopwatch.StartNew(); + + foreach (var (key, value) in AdditionalQueryParameters) request.RequestUri = request.RequestUri.AddQuery(key, value); + foreach (var (key, value) in DefaultRequestHeaders) { + if (request.Headers.Contains(key)) continue; + request.Headers.Add(key, value); + } + + request.Options.Set(new HttpRequestOptionsKey("WebAssemblyEnableStreamingResponse"), true); + + if (LogRequests) + Console.WriteLine("Sending " + request.Summarise(includeHeaders: true, includeQuery: true, includeContentIfText: false, hideHeaders: ["Accept"])); + + HttpResponseMessage? responseMessage; + try { + responseMessage = await Client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken); + } + catch (Exception e) { + if (e is TaskCanceledException or TimeoutException) { + if (request.Method == HttpMethod.Get && !cancellationToken.IsCancellationRequested) { + await Task.Delay(Random.Shared.Next(500, 2500), cancellationToken); + request.ResetSendStatus(); + return await SendAsync(request, cancellationToken); + } + } + else if (!e.ToString().StartsWith("TypeError: NetworkError")) + Console.WriteLine( + $"Failed to send request {request.Method} {BaseAddress}{request.RequestUri} ({Util.BytesToString(request.Content?.Headers.ContentLength ?? 0)}):\n{e}"); + throw; + } +#if SYNC_HTTPCLIENT + finally { + _rateLimitSemaphore.Release(); + } +#endif + + // Console.WriteLine($"Sending {request.Method} {request.RequestUri} ({Util.BytesToString(request.Content?.Headers.ContentLength ?? 0)}) -> {(int)responseMessage.StatusCode} {responseMessage.StatusCode} ({Util.BytesToString(responseMessage.GetContentLength())}, WAIT={swWait.ElapsedMilliseconds}ms, EXEC={swExec.ElapsedMilliseconds}ms)"); + if (LogRequests) + Console.WriteLine("Received " + responseMessage.Summarise(includeHeaders: true, includeContentIfText: false, hideHeaders: [ + "Server", + "Date", + "Transfer-Encoding", + "Connection", + "Vary", + "Content-Length", + "Access-Control-Allow-Origin", + "Access-Control-Allow-Methods", + "Access-Control-Allow-Headers", + "Access-Control-Expose-Headers", + "Cache-Control", + "Cross-Origin-Resource-Policy", + "X-Content-Security-Policy", + "Referrer-Policy", + "X-Robots-Tag", + "Content-Security-Policy" + ])); + + return responseMessage; + } + + public async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken = default) { + var responseMessage = await SendUnhandledAsync(request, cancellationToken); + if (responseMessage.IsSuccessStatusCode) return responseMessage; + + //retry on gateway timeout + // if (responseMessage.StatusCode == HttpStatusCode.GatewayTimeout) { + // request.ResetSendStatus(); + // return await SendAsync(request, cancellationToken); + // } + + //error handling + var content = await responseMessage.Content.ReadAsStringAsync(cancellationToken); + if (content.Length == 0) + throw new DataException("Content was empty"); + // throw new MatrixException() { + // ErrorCode = "M_UNKNOWN", + // Error = "Unknown error, server returned no content" + // }; + + // if (!content.StartsWith('{')) throw new InvalidDataException("Encountered invalid data:\n" + content); + if (!content.TrimStart().StartsWith('{')) { + responseMessage.EnsureSuccessStatusCode(); + throw new InvalidDataException("Encountered invalid data:\n" + content); + } + //we have a matrix error + + throw new Exception("Unknown http exception"); + // MatrixException? ex; + // try { + // ex = JsonSerializer.Deserialize(content); + // } + // catch (JsonException e) { + // throw new LibMatrixException() { + // ErrorCode = "M_INVALID_JSON", + // Error = e.Message + "\nBody:\n" + await responseMessage.Content.ReadAsStringAsync(cancellationToken) + // }; + // } + // + // Debug.Assert(ex != null, nameof(ex) + " != null"); + // ex.RawContent = content; + // // Console.WriteLine($"Failed to send request: {ex}"); + // if (ex.RetryAfterMs is null) throw ex!; + // //we have a ratelimit error + // await Task.Delay(ex.RetryAfterMs.Value, cancellationToken); + request.ResetSendStatus(); + return await SendAsync(request, cancellationToken); + } + + // GetAsync + public Task GetAsync([StringSyntax("Uri")] string? requestUri, CancellationToken? cancellationToken = null) => + SendAsync(new HttpRequestMessage(HttpMethod.Get, requestUri), cancellationToken ?? CancellationToken.None); + + // GetFromJsonAsync + public async Task TryGetFromJsonAsync(string requestUri, JsonSerializerOptions? options = null, CancellationToken cancellationToken = default) { + try { + return await GetFromJsonAsync(requestUri, options, cancellationToken); + } + catch (JsonException e) { + Console.WriteLine($"Failed to deserialize response from {requestUri}: {e.Message}"); + return default; + } + catch (HttpRequestException e) { + Console.WriteLine($"Failed to get {requestUri}: {e.Message}"); + return default; + } + } + + public async Task GetFromJsonAsync(string requestUri, JsonSerializerOptions? options = null, CancellationToken cancellationToken = default) { + options = GetJsonSerializerOptions(options); + var request = new HttpRequestMessage(HttpMethod.Get, requestUri); + request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); + var response = await SendAsync(request, cancellationToken); + response.EnsureSuccessStatusCode(); + await using var responseStream = await response.Content.ReadAsStreamAsync(cancellationToken); + + return await JsonSerializer.DeserializeAsync(responseStream, options, cancellationToken) ?? + throw new InvalidOperationException("Failed to deserialize response"); + } + + // GetStreamAsync + public async Task GetStreamAsync(string requestUri, CancellationToken cancellationToken = default) { + var request = new HttpRequestMessage(HttpMethod.Get, requestUri); + request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); + var response = await SendAsync(request, cancellationToken); + response.EnsureSuccessStatusCode(); + return await response.Content.ReadAsStreamAsync(cancellationToken); + } + + public async Task PutAsJsonAsync([StringSyntax(StringSyntaxAttribute.Uri)] string? requestUri, T value, JsonSerializerOptions? options = null, + CancellationToken cancellationToken = default) where T : notnull { + options = GetJsonSerializerOptions(options); + var request = new HttpRequestMessage(HttpMethod.Put, requestUri); + request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); + request.Content = new StringContent(JsonSerializer.Serialize(value, value.GetType(), options), + Encoding.UTF8, "application/json"); + return await SendAsync(request, cancellationToken); + } + + public async Task PostAsJsonAsync([StringSyntax(StringSyntaxAttribute.Uri)] string? requestUri, T value, JsonSerializerOptions? options = null, + CancellationToken cancellationToken = default) where T : notnull { + options ??= new JsonSerializerOptions(); + // options.Converters.Add(new JsonFloatStringConverter()); + // options.Converters.Add(new JsonDoubleStringConverter()); + // options.Converters.Add(new JsonDecimalStringConverter()); + options.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull; + var request = new HttpRequestMessage(HttpMethod.Post, requestUri); + request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); + request.Content = new StringContent(JsonSerializer.Serialize(value, value.GetType(), options), + Encoding.UTF8, "application/json"); + return await SendAsync(request, cancellationToken); + } + + public async IAsyncEnumerable GetAsyncEnumerableFromJsonAsync([StringSyntax(StringSyntaxAttribute.Uri)] string? requestUri, JsonSerializerOptions? options = null) { + options = GetJsonSerializerOptions(options); + var res = await GetAsync(requestUri); + options.PropertyNameCaseInsensitive = true; + var result = JsonSerializer.DeserializeAsyncEnumerable(await res.Content.ReadAsStreamAsync(), options); + await foreach (var resp in result) yield return resp; + } + + public static async Task CheckSuccessStatus(string url) { + //cors causes failure, try to catch + try { + var resp = await Client.GetAsync(url); + return resp.IsSuccessStatusCode; + } + catch (Exception e) { + Console.WriteLine($"Failed to check success status: {e.Message}"); + return false; + } + } + + public async Task PostAsync(string uri, HttpContent? content, CancellationToken cancellationToken = default) { + var request = new HttpRequestMessage(HttpMethod.Post, uri) { + Content = content + }; + return await SendAsync(request, cancellationToken); + } + + public async Task DeleteAsync(string url) { + var request = new HttpRequestMessage(HttpMethod.Delete, url); + await SendAsync(request); + } + + public async Task DeleteAsJsonAsync(string url, T payload) { + var request = new HttpRequestMessage(HttpMethod.Delete, url) { + Content = new StringContent(JsonSerializer.Serialize(payload), Encoding.UTF8, "application/json") + }; + return await SendAsync(request); + } +} +#endif diff --git a/extra/admin-api/Utilities/Spacebar.AdminApi.PrepareTestData/Constants.cs b/extra/admin-api/Utilities/Spacebar.AdminApi.PrepareTestData/Constants.cs new file mode 100644 index 000000000..02d3941f1 --- /dev/null +++ b/extra/admin-api/Utilities/Spacebar.AdminApi.PrepareTestData/Constants.cs @@ -0,0 +1,7 @@ +namespace Spacebar.AdminApi.PrepareTestData; + +public class Constants { + public const string ApiBaseUrl = "http://localhost:3001/api/v9"; + public const string CdnBaseUrl = "http://localhost:3003/"; + // public const string ApiBaseUrl = "http://localhost:3001/api/v9"; +} \ No newline at end of file diff --git a/extra/admin-api/Utilities/Spacebar.AdminApi.PrepareTestData/Program.cs b/extra/admin-api/Utilities/Spacebar.AdminApi.PrepareTestData/Program.cs index 4bfb037a9..268066fea 100644 --- a/extra/admin-api/Utilities/Spacebar.AdminApi.PrepareTestData/Program.cs +++ b/extra/admin-api/Utilities/Spacebar.AdminApi.PrepareTestData/Program.cs @@ -1,8 +1,17 @@ // See https://aka.ms/new-console-template for more information using ArcaneLibs; +using Spacebar.AdminApi.PrepareTestData; using Spacebar.AdminApi.PrepareTestData.TestDataTypes; +await Utils.PostFileWithDataAsync("http://localhost:3001/api/v9/channels/1324497120836834414/messages", + "eyJhbGciOiJFUzUxMiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjExODM1Njg3NTA5MzEwOTk2NzkiLCJpYXQiOjE3NDQyMTMxNzcsImtpZCI6IjdiMWM5OTBhMWQ1ZWI3MDVjMWFjNmIxOWYwNTVmMTM5Y2FiZDhhOTZmMzg3YTU1NDM3MDRhZDY0OTMyMzViYTMifQ.AUf87OS5DsWLfBR9VVF7emOE8cG8B4JLvktr2WxF9_XQsPd0X8da2s9f9Lq5pTmYe9zOaI7DrHMuggih3uZ9NmZzAfeasEgew4gCBKIcvhxSaKWcU9DMVHgZl-ZH5HnB0yk8l5IKzIV3z6wt9Ght-F_g5SRZiNlpthva0jU2QhRro3IB", + new { + content = "Hello world", + nonce = Random.Shared.NextInt64() + }, + File.ReadAllBytes("/home/Rory/Documents/kuromi_smug.png"), "test.png", "image/png"); +return; Console.WriteLine("Hello, World!"); var tests = ClassCollector.ResolveFromAllAccessibleAssemblies(); foreach (var test in tests) { @@ -19,6 +28,7 @@ if (runMethod != null) { var task = runMethod.Invoke(testToRun, null) as Task; await task!; Console.WriteLine($"Test {testToRun.FullName} completed."); -} else { +} +else { Console.WriteLine("Test not found."); } \ No newline at end of file diff --git a/extra/admin-api/Utilities/Spacebar.AdminApi.PrepareTestData/TestDataTypes/AttachmentSpamTestData.cs b/extra/admin-api/Utilities/Spacebar.AdminApi.PrepareTestData/TestDataTypes/AttachmentSpamTestData.cs new file mode 100644 index 000000000..114d45a2e --- /dev/null +++ b/extra/admin-api/Utilities/Spacebar.AdminApi.PrepareTestData/TestDataTypes/AttachmentSpamTestData.cs @@ -0,0 +1,87 @@ +using System.Net.Http.Json; +using System.Text.Json.Nodes; + +namespace Spacebar.AdminApi.PrepareTestData.TestDataTypes; + +public class AttachmentSpamTestData : ITestData { + public static async Task Run() { + using var hc = new HttpClient(); + var token = await Utils.CreateUser(); + hc.DefaultRequestHeaders.Authorization = new("Bearer", token); + + int guildCount = 1; + int channelCount = 100; // per guild + int messageCount = 1000; // per channel + + for (int guild = 0; guild < guildCount; guild++) { + var guildId = await Utils.CreateGuild(token); + // for (int channel = 0; channel < channelCount; channel++) { + // Console.WriteLine($"> Creating channel {channel} in guild {guild}..."); + // var channelRequest = await hc.PostAsJsonAsync($"http://localhost:3001/api/v9/guilds/{guildId}/channels", new { + // name = Guid.NewGuid().ToString()[0..30], + // type = 0 + // }); + // var channelResponse = await channelRequest.Content.ReadFromJsonAsync(); + // var channelId = channelResponse!["id"]!.ToString(); + // await SendMessages(hc, channelId, messageCount); + // } + + var ss = new SemaphoreSlim(16,16); + var tasks = Enumerable.Range(0, channelCount).Select(async channel => { + await ss.WaitAsync(); + Console.WriteLine($"> Creating channel {channel} in guild {guildId}..."); + var channelRequest = await hc.PostAsJsonAsync($"{Constants.ApiBaseUrl}/guilds/{guildId}/channels", new { + name = Guid.NewGuid().ToString()[0..30], + type = 0 + }); + var channelResponse = await channelRequest.Content.ReadFromJsonAsync(); + var channelId = channelResponse!["id"]!.ToString(); + await SendMessages(hc, channelId, messageCount); + ss.Release(); + }); + await Task.WhenAll(tasks); + } + } + + private static async Task CreateChannels(HttpClient hc, string guildId, int channelCount) { + var tasks = Enumerable.Range(0, channelCount).Select(async channel => { + Console.WriteLine($"> Creating channel {channel} in guild {guildId}..."); + var channelRequest = await hc.PostAsJsonAsync($"http://localhost:3001/api/v9/guilds/{guildId}/channels", new { + name = Guid.NewGuid().ToString()[0..30], + type = 0 + }); + var channelResponse = await channelRequest.Content.ReadFromJsonAsync(); + }); + await Task.WhenAll(tasks); + } + + private static async Task SendMessages(HttpClient hc, string channelId, int maxMessageCount) { + // var ss = new SemaphoreSlim(32, 32); + // var tasks = Enumerable.Range(0, Random.Shared.Next((int)(0.75 * maxMessageCount), maxMessageCount)).Select(async message => { + // var success = false; + // while (!success) { + // await ss.WaitAsync(); + // Console.WriteLine($"> Sending message {message} in channel {channelId}..."); + // var messageReq = await hc.PostAsJsonAsync($"http://localhost:3001/api/v9/channels/{channelId}/messages", new { + // content = Guid.NewGuid().ToString() + // }); + // var messageResponse = await messageReq.Content.ReadFromJsonAsync(); + // if (messageResponse.ContainsKey("id")) { + // success = true; + // Console.WriteLine(messageResponse!["id"]!.ToString()); + // } + // } + // + // ss.Release(); + // }); + // await Task.WhenAll(tasks); + + var messageReq = await hc.PostAsJsonAsync($"http://localhost:3001/api/v9/channels/{channelId}/messages", new { + content = Guid.NewGuid().ToString() + }); + var messageResponse = await messageReq.Content.ReadFromJsonAsync(); + if (messageResponse.ContainsKey("id")) { + await hc.GetAsync($"http://localhost:5112/Users/duplicate/{messageResponse!["id"]!.ToString()}?count={maxMessageCount}"); + } + } +} \ No newline at end of file diff --git a/extra/admin-api/Utilities/Spacebar.AdminApi.PrepareTestData/Utils.cs b/extra/admin-api/Utilities/Spacebar.AdminApi.PrepareTestData/Utils.cs index 143f50a44..398dbaf2a 100644 --- a/extra/admin-api/Utilities/Spacebar.AdminApi.PrepareTestData/Utils.cs +++ b/extra/admin-api/Utilities/Spacebar.AdminApi.PrepareTestData/Utils.cs @@ -1,5 +1,9 @@ +using System.Net; +using System.Net.Http.Headers; using System.Net.Http.Json; +using System.Text.Json; using System.Text.Json.Nodes; +using System.Text.Json.Serialization; using ArcaneLibs.Extensions; namespace Spacebar.AdminApi.PrepareTestData; @@ -8,7 +12,7 @@ public static class Utils { public static async Task CreateUser() { Console.WriteLine("> Creating user..."); using var hc = new HttpClient(); - var registerRequest = await hc.PostAsJsonAsync("http://localhost:3001/api/v9/auth/register", new { + var registerRequest = await hc.PostAsJsonAsync($"{Constants.ApiBaseUrl}/auth/register", new { username = Guid.NewGuid().ToString()[0..30], password = "password", email = $"{Guid.NewGuid()}@example.com", @@ -22,14 +26,36 @@ public static class Utils { public static async Task CreateGuild(string token) { using var hc = new HttpClient(); hc.DefaultRequestHeaders.Authorization = new("Bearer", token); - + Console.WriteLine("> Creating guild..."); - var guildRequest = await hc.PostAsJsonAsync("http://localhost:3001/api/v9/guilds", new { - name = Guid.NewGuid().ToString()[0..30] + var guildRequest = await hc.PostAsJsonAsync($"{Constants.ApiBaseUrl}/guilds", new { + name = Guid.NewGuid().ToString()[..30] }); - + var guildResponse = await guildRequest.Content.ReadFromJsonAsync(); var guildId = guildResponse!["id"]!.ToString(); return guildId; } + + public static async Task PostFileWithDataAsync(string url, string token, object data, byte[] file, string filename, string contentType) { + try { + using var hc = new HttpClient(); + hc.DefaultRequestHeaders.Authorization = new("Bearer", token); + var f = new MultipartFormDataContent(); + // f.Add(new StringContent()); + f.Add(JsonContent.Create(data), "payload_json"); + f.Add(new ByteArrayContent(file) { + Headers = { ContentType = new MediaTypeHeaderValue(contentType) } + }, "files[0]", filename); + + var _resp = await hc.PostAsync(url, f); + var resp = await _resp.Content.ReadAsStringAsync(); + + Console.WriteLine(resp); + } + catch (Exception e) { + Console.WriteLine(e); + throw; + } + } } \ No newline at end of file -- cgit 1.5.1 From e64583304271c7c2604f41df5ce89cbed42c2cb5 Mon Sep 17 00:00:00 2001 From: "Emma [it/its]@Rory&" Date: Sun, 6 Jul 2025 16:15:15 +0200 Subject: More admin API fun --- extra/admin-api/.gitignore | 6 +- .../Controllers/UserController.cs | 94 +++++++++++++++++++++- .../Middleware/AuthenticationMiddleware.cs | 4 +- .../Spacebar.AdminAPI/Services/Configuration.cs | 2 + .../RabbitMQConfiguration.cs | 5 ++ 5 files changed, 105 insertions(+), 6 deletions(-) (limited to 'extra/admin-api/Spacebar.AdminAPI/Controllers/UserController.cs') diff --git a/extra/admin-api/.gitignore b/extra/admin-api/.gitignore index 5a9afc4c7..f4e4961b6 100644 --- a/extra/admin-api/.gitignore +++ b/extra/admin-api/.gitignore @@ -1,9 +1,9 @@ -**/bin/ -**/obj/ +bin/ +obj/ /patches/ appsettings.Local*.json *.DotSettings.user /*.patch Spacebar.Db/**/*.orig -Spacebar.Db/**/*.rej \ No newline at end of file +Spacebar.Db/**/*.rej diff --git a/extra/admin-api/Spacebar.AdminAPI/Controllers/UserController.cs b/extra/admin-api/Spacebar.AdminAPI/Controllers/UserController.cs index 620382e1f..622909915 100644 --- a/extra/admin-api/Spacebar.AdminAPI/Controllers/UserController.cs +++ b/extra/admin-api/Spacebar.AdminAPI/Controllers/UserController.cs @@ -1,9 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; using System.Text.Json.Serialization; +using System.Threading; +using System.Threading.Tasks; +using ArcaneLibs; using ArcaneLibs.Extensions; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; using RabbitMQ.Client; using Spacebar.AdminApi.Models; +using Spacebar.AdminAPI.Services; using Spacebar.Db.Contexts; using Spacebar.Db.Models; using Spacebar.RabbitMqUtilities; @@ -12,7 +22,7 @@ namespace Spacebar.AdminAPI.Controllers; [ApiController] [Route("/Users")] -public class UserController(ILogger logger, SpacebarDbContext db, RabbitMQService mq, IServiceProvider sp) : ControllerBase { +public class UserController(ILogger logger, Configuration config, RabbitMQConfiguration amqpConfig, SpacebarDbContext db, RabbitMQService mq, IServiceProvider sp) : ControllerBase { private readonly ILogger _logger = logger; [HttpGet] @@ -408,4 +418,86 @@ public class UserController(ILogger logger, SpacebarDbContext db } } } + + // { + // "op": 0, + // "t": "GUILD_ROLE_UPDATE", + // "d": { + // "guild_id": "1006649183970562092", + // "role": { + // "id": "1006706520514028812", + // "guild_id": "1006649183970562092", + // "color": 16711680, + // "hoist": true, + // "managed": false, + // "mentionable": true, + // "name": "Adminstrator", + // "permissions": "9", + // "position": 5, + // "unicode_emoji": "💖", + // "flags": 0 + // } + // }, + // "s": 38 + // } + + [HttpGet("test")] + public async IAsyncEnumerable Test() { + var factory = new ConnectionFactory { + Uri = new Uri(amqpConfig.ToConnectionString()) + }; + await using var mqConnection = await factory.CreateConnectionAsync(); + await using var mqChannel = await mqConnection.CreateChannelAsync(); + + var guildId = "1006649183970562092"; + // var roleId = "1006706520514028812"; //Administrator + var roleId = "1391303296148639051"; //Spacebar Maintainer + // int color = 16711680; //Administrator + int color = 99839; //Spacebar Maintainer + + await mqChannel.ExchangeDeclareAsync(exchange: guildId, type: ExchangeType.Fanout, durable: false); + + var props = new BasicProperties() { Type = "GUILD_ROLE_UPDATE" }; + int framerate = 30; + float delay = 1000f / framerate; + var secondsPerRotation = 6.243f; + // use delay, 255f = one rotation, lengthFactor = iterations to make a full rotation + var lengthFactor = (secondsPerRotation * 1000f / delay); + Console.WriteLine("Length factor: {0}, RPS: {1}", lengthFactor, 0); + var re = new RainbowEnumerator(lengthFactor: lengthFactor, offset: color, skip: 1); + var sw = Stopwatch.StartNew(); + while (true) { + var clr = re.Next(); + color = clr.r << 16 | clr.g << 8 | clr.b; + var publishSuccess = false; + do { + try { + await mqChannel.BasicPublishAsync(exchange: guildId, routingKey: "", mandatory: false, basicProperties: props, body: new { + guild_id = guildId, + role = new { + id = roleId, + guild_id = guildId, + color, + hoist = false, + managed = false, + mentionable = true, + name = "Spacebar Maintainer", + permissions = "8", + position = 5, + unicode_emoji = "", + flags = 0 + } + }.ToJson().AsBytes().ToArray()); + publishSuccess = true; + } + catch (Exception e) { + Console.WriteLine($"[RabbitMQ] Error publishing bulk delete: {e.Message}"); + await Task.Delay(10); + } + } while (!publishSuccess); + yield return $"{clr.r:X2} {clr.g:X2} {clr.b:X2} | {color:X8} | {sw.Elapsed} (waiting {Math.Max(0, (int)delay - (int)sw.ElapsedMilliseconds)} out of {delay} ms)"; + await Task.Delay(Math.Max(0, (int)delay - (int)sw.ElapsedMilliseconds)); + sw.Restart(); + } + } } \ No newline at end of file diff --git a/extra/admin-api/Spacebar.AdminAPI/Middleware/AuthenticationMiddleware.cs b/extra/admin-api/Spacebar.AdminAPI/Middleware/AuthenticationMiddleware.cs index 2dc4c9774..1c8f9e939 100644 --- a/extra/admin-api/Spacebar.AdminAPI/Middleware/AuthenticationMiddleware.cs +++ b/extra/admin-api/Spacebar.AdminAPI/Middleware/AuthenticationMiddleware.cs @@ -13,7 +13,8 @@ public class AuthenticationMiddleware(RequestDelegate next) { private static Dictionary _userCacheExpiry = new(); public async Task InvokeAsync(HttpContext context, IServiceProvider sp) { - if (context.Request.Path.StartsWithSegments("/ping")) { + var config = sp.GetRequiredService(); + if (context.Request.Path.StartsWithSegments("/ping") || config.DisableAuthentication) { await next(context); return; } @@ -55,7 +56,6 @@ public class AuthenticationMiddleware(RequestDelegate next) { if (!_userCache.ContainsKey(token)) { var db = sp.GetRequiredService(); - var config = sp.GetRequiredService(); user = await db.Users.FindAsync(config.OverrideUid ?? res.ClaimsIdentity.Claims.First(x => x.Type == "id").Value) ?? throw new InvalidOperationException(); _userCache[token] = user; diff --git a/extra/admin-api/Spacebar.AdminAPI/Services/Configuration.cs b/extra/admin-api/Spacebar.AdminAPI/Services/Configuration.cs index a988a316e..a58ef5c07 100644 --- a/extra/admin-api/Spacebar.AdminAPI/Services/Configuration.cs +++ b/extra/admin-api/Spacebar.AdminAPI/Services/Configuration.cs @@ -6,4 +6,6 @@ public class Configuration { } public string? OverrideUid { get; set; } + public bool DisableAuthentication { get; set; } = false; + public bool Enforce2FA { get; set; } = true; } \ No newline at end of file diff --git a/extra/admin-api/Utilities/Spacebar.RabbitMqUtilities/RabbitMQConfiguration.cs b/extra/admin-api/Utilities/Spacebar.RabbitMqUtilities/RabbitMQConfiguration.cs index e31e8c365..78a9add15 100644 --- a/extra/admin-api/Utilities/Spacebar.RabbitMqUtilities/RabbitMQConfiguration.cs +++ b/extra/admin-api/Utilities/Spacebar.RabbitMqUtilities/RabbitMQConfiguration.cs @@ -9,4 +9,9 @@ public class RabbitMQConfiguration { public required string Host { get; set; } public required string Username { get; set; } public required string Password { get; set; } + public required string Port { get; set; } + + public string ToConnectionString() { + return $"amqp://{Username}:{Password}@{Host}:{Port}"; + } } \ No newline at end of file -- cgit 1.5.1
@column.GetValue(user)