diff --git a/.gitignore b/.gitignore index 0fcd6d2d..902ed77c 100644 --- a/.gitignore +++ b/.gitignore @@ -19,4 +19,5 @@ build *.tmp tmp/ dump/ -result \ No newline at end of file +result +jwt.key* \ No newline at end of file diff --git a/.idea/jsLibraryMappings.xml b/.idea/jsLibraryMappings.xml new file mode 100644 index 00000000..d23208fb --- /dev/null +++ b/.idea/jsLibraryMappings.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml index d35534a7..fbd90b4d 100644 --- a/.idea/modules.xml +++ b/.idea/modules.xml @@ -2,7 +2,7 @@ - + \ No newline at end of file diff --git a/.idea/runConfigurations/Start_API.xml b/.idea/runConfigurations/Start_API.xml new file mode 100644 index 00000000..edb214cf --- /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 00000000..78aedd2b --- /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 00000000..4917f434 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 00000000..536d3f71 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 00000000..8f88c546 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 00000000..d1478bac 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 00000000..b97239f3 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 00000000..be01aaca 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 00000000..d60c270e 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 00000000..65384987 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 00000000..66ca3d2b 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 00000000..669d04cd 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 00000000..80cfd15e 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 00000000..40da4276 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 00000000..5ead7b0d 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 00000000..c5dd294b 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 00000000..17270e45 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 00000000..a6432151 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 00000000..00900e0a --- /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 00000000..d9a0370a --- /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/Constants.cs b/extra/admin-api/Utilities/Spacebar.AdminApi.PrepareTestData/Constants.cs new file mode 100644 index 00000000..02d3941f --- /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 new file mode 100644 index 00000000..268066fe --- /dev/null +++ b/extra/admin-api/Utilities/Spacebar.AdminApi.PrepareTestData/Program.cs @@ -0,0 +1,34 @@ +// 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) { + 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 00000000..de3115ae --- /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/AttachmentSpamTestData.cs b/extra/admin-api/Utilities/Spacebar.AdminApi.PrepareTestData/TestDataTypes/AttachmentSpamTestData.cs new file mode 100644 index 00000000..114d45a2 --- /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/TestDataTypes/ITestData.cs b/extra/admin-api/Utilities/Spacebar.AdminApi.PrepareTestData/TestDataTypes/ITestData.cs new file mode 100644 index 00000000..4c667c65 --- /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 00000000..5ac792ad --- /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 00000000..398dbaf2 --- /dev/null +++ b/extra/admin-api/Utilities/Spacebar.AdminApi.PrepareTestData/Utils.cs @@ -0,0 +1,61 @@ +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; + +public static class Utils { + public static async Task CreateUser() { + Console.WriteLine("> Creating user..."); + using var hc = new HttpClient(); + var registerRequest = await hc.PostAsJsonAsync($"{Constants.ApiBaseUrl}/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($"{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 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 00000000..78a9add1 --- /dev/null +++ b/extra/admin-api/Utilities/Spacebar.RabbitMqUtilities/RabbitMQConfiguration.cs @@ -0,0 +1,17 @@ +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; } + public required string Port { get; set; } + + public string ToConnectionString() { + return $"amqp://{Username}:{Password}@{Host}:{Port}"; + } +} \ 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 00000000..22255942 --- /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 00000000..be1f4a77 --- /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/db-patches/db-00-fix-flags.patch b/extra/admin-api/db-patches/db-00-fix-flags.patch new file mode 100644 index 00000000..6f79703b --- /dev/null +++ b/extra/admin-api/db-patches/db-00-fix-flags.patch @@ -0,0 +1,15 @@ +diff --git a/extra/admin-api/Spacebar.Db/Models/User.cs b/extra/admin-api/Spacebar.Db/Models/User.cs +index 7825bd17..ca140dbc 100644 +--- a/extra/admin-api/Spacebar.Db/Models/User.cs ++++ b/extra/admin-api/Spacebar.Db/Models/User.cs +@@ -92,8 +92,8 @@ public partial class User + [Column("email", TypeName = "character varying")] + public string? Email { get; set; } + +- [Column("flags")] +- public int Flags { get; set; } ++ [Column("flags", TypeName = "character varying")] ++ public string Flags { get; set; } + + [Column("public_flags")] + public int PublicFlags { get; set; } diff --git a/extra/admin-api/flake.nix b/extra/admin-api/flake.nix new file mode 100644 index 00000000..6cf35682 --- /dev/null +++ b/extra/admin-api/flake.nix @@ -0,0 +1,92 @@ +{ + description = "Spacebar Admin API, written in C#."; + + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + flake-utils.url = "github:numtide/flake-utils"; + }; + + outputs = + { + self, + nixpkgs, + flake-utils, + }: + flake-utils.lib.eachSystem flake-utils.lib.allSystems ( + system: + let + pkgs = import nixpkgs { + inherit system; + }; + hashesFile = builtins.fromJSON (builtins.readFile ./hashes.json); + lib = pkgs.lib; + in + { + packages = { + default = pkgs.buildNpmPackage { + pname = "spacebar-server-ts"; + name = "spacebar-server-ts"; + + meta = with lib; { + description = "Spacebar server, a FOSS reimplementation of the Discord backend."; + homepage = "https://github.com/spacebarchat/server"; + license = licenses.agpl3Plus; + platforms = platforms.all; + mainProgram = "start-bundle"; + }; + + src = ./.; + nativeBuildInputs = with pkgs; [ python3 ]; + npmDepsHash = hashesFile.npmDepsHash; + makeCacheWritable = true; + postPatch = '' + substituteInPlace package.json --replace 'npx patch-package' '${pkgs.nodePackages.patch-package}/bin/patch-package' + ''; + installPhase = '' + runHook preInstall + set -x + #remove packages not needed for production, or at least try to... + npm prune --omit dev --no-save $npmInstallFlags "''${npmInstallFlagsArray[@]}" $npmFlags "''${npmFlagsArray[@]}" + find node_modules -maxdepth 1 -type d -empty -delete + + mkdir -p $out + cp -r assets dist node_modules package.json $out/ + for i in dist/**/start.js + do + makeWrapper ${pkgs.nodejs}/bin/node $out/bin/start-`dirname ''${i/dist\//}` --prefix NODE_PATH : $out/node_modules --add-flags $out/$i + done + + set +x + runHook postInstall + ''; + }; + + update-nix = pkgs.writeShellApplication { + name = "update-nix"; + runtimeInputs = with pkgs; [ + prefetch-npm-deps + nix + jq + ]; + text = '' + nix flake update --extra-experimental-features 'nix-command flakes' + DEPS_HASH=$(prefetch-npm-deps package-lock.json) + TMPFILE=$(mktemp) + jq '.npmDepsHash = "'"$DEPS_HASH"'"' hashes.json > "$TMPFILE" + mv -- "$TMPFILE" hashes.json + ''; + }; + }; + + devShell = pkgs.mkShell { + buildInputs = with pkgs; [ + nodejs + nodePackages.typescript + nodePackages.ts-node + nodePackages.patch-package + nodePackages.prettier + ]; + }; + } + ); +} diff --git a/extra/admin-api/scaffold-db b/extra/admin-api/scaffold-db new file mode 100755 index 00000000..0f332e00 --- /dev/null +++ b/extra/admin-api/scaffold-db @@ -0,0 +1,33 @@ +#! /usr/bin/env nix-shell +#! nix-shell -i nix -p nix +#! nix shell nixpkgs#bash nixpkgs#dotnet-ef nixpkgs#postgresql --command bash + +set -ex +rm -rfv Spacebar.Db + +# prep temporary db +# - Update collation version for template1 just incase! +psql -U postgres -c 'ALTER DATABASE template1 REFRESH COLLATION VERSION;' +dropdb -U postgres sb-server-scaffold --if-exists --force || true +createdb -U postgres sb-server-scaffold +DATABASE=postgres://postgres@127.0.0.1/sb-server-scaffold nix shell nixpkgs#nodejs ../.. --command npm run sync:db + +# Create new project +dotnet new classlib --no-restore -o Spacebar.Db +cd Spacebar.Db +rm Class1.cs +dotnet add package Npgsql.EntityFrameworkCore.PostgreSQL -n -f net9.0 +dotnet add package Microsoft.EntityFrameworkCore.Design -n -f net9.0 + +dotnet-ef dbcontext scaffold "Host=127.0.0.1; Username=postgres; Database=sb-server-scaffold" \ + Npgsql.EntityFrameworkCore.PostgreSQL \ + -o Models \ + -c SpacebarDbContext \ + --context-dir Contexts \ + --force \ + --no-onconfiguring \ + --data-annotations + +for patch in db-patches/*.patch; do + patch -p3 < $patch +done \ No newline at end of file diff --git a/src/api/middlewares/CORS.ts b/src/api/middlewares/CORS.ts index 3e7452fc..ca6abb82 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 3f3164fc..09e8805f 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 97bdec74..3de05aa2 100644 --- a/src/util/util/Token.ts +++ b/src/util/util/Token.ts @@ -19,19 +19,33 @@ import jwt, { VerifyOptions } from "jsonwebtoken"; import { Config } from "./Config"; import { User } from "../entities"; +import crypto from "node:crypto"; +import fs from "fs/promises"; +import { existsSync } from "fs"; // TODO: dont use deprecated APIs lol import { FindOptionsRelationByString, FindOptionsSelectByString, } from "typeorm"; +import * as console from "node:console"; export const JWTOptions: VerifyOptions = { algorithms: ["HS256"] }; export type UserTokenData = { user: User; - decoded: { id: string; iat: number; email?: string }; + 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?: { @@ -43,56 +57,87 @@ export const checkToken = ( token = token.replace("Bot ", ""); // there is no bot distinction in sb token = token.replace("Bearer ", ""); // allow bearer tokens - jwt.verify( - token, - Config.get().security.jwtSecret, - JWTOptions, - async (err, out) => { - const decoded = out as UserTokenData["decoded"]; - if (err || !decoded) return reject("Invalid Token"); + const validateUser: jwt.VerifyCallback = async (err, out) => { + const decoded = out as UserTokenData["decoded"]; + if (err || !decoded) { + logAuth("validateUser rejected: " + err); + return rejectAndLog(reject, "Invalid Token meow " + err); + } - const user = await User.findOne({ - where: decoded.email - ? { email: decoded.email } - : { id: decoded.id }, - select: [ - ...(opts?.select || []), - "bot", - "disabled", - "deleted", - "rights", - "data", - ], - relations: opts?.relations, - }); + const user = await User.findOne({ + where: { id: decoded.id }, + select: [ + ...(opts?.select || []), + "id", + "bot", + "disabled", + "deleted", + "rights", + "data", + ], + 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"); + // 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) + ) { + logAuth("validateUser rejected: Token not yet valid"); + return rejectAndLog(reject, "Invalid Token"); + } - if (user.disabled) return reject("User disabled"); - if (user.deleted) return reject("User not found"); + 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"); + } - return resolve({ decoded, user }); - }, - ); + 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( + token, + Config.get().security.jwtSecret, + JWTOptions, + validateUser, + ); + } else if (dec.header.alg == "ES512") { + loadOrGenerateKeypair().then((keyPair) => { + jwt.verify( + token, + keyPair.publicKey, + { algorithms: ["ES512"] }, + validateUser, + ); + }); + } else return reject("Invalid token algorithm"); }); -export async function generateToken(id: string, email?: string) { +export async function generateToken(id: string) { const iat = Math.floor(Date.now() / 1000); - const algorithm = "HS256"; + const keyPair = await loadOrGenerateKeypair(); return new Promise((res, rej) => { jwt.sign( - { id, iat, email }, - Config.get().security.jwtSecret, + { id, iat, kid: keyPair.fingerprint }, + keyPair.privateKey, { - algorithm, + algorithm: "ES512", }, (err, token) => { if (err) return rej(err); @@ -101,3 +146,44 @@ export async function generateToken(id: string, email?: string) { ); }); } + +// Get ECDSA keypair from file or generate it +export async function loadOrGenerateKeypair() { + let privateKey: crypto.KeyObject; + let publicKey: crypto.KeyObject; + + if (existsSync("jwt.key") && existsSync("jwt.key.pub")) { + const [loadedPrivateKey, loadedPublicKey] = await Promise.all([ + fs.readFile("jwt.key"), + fs.readFile("jwt.key.pub"), + ]); + + privateKey = crypto.createPrivateKey(loadedPrivateKey); + publicKey = crypto.createPublicKey(loadedPublicKey); + } else { + console.log("[JWT] Generating new keypair"); + const res = crypto.generateKeyPairSync("ec", { + namedCurve: "secp521r1", + }); + privateKey = res.privateKey; + publicKey = res.publicKey; + + await Promise.all([ + fs.writeFile( + "jwt.key", + privateKey.export({ format: "pem", type: "sec1" }), + ), + fs.writeFile( + "jwt.key.pub", + publicKey.export({ format: "pem", type: "spki" }), + ), + ]); + } + + const fingerprint = crypto + .createHash("sha256") + .update(publicKey.export({ format: "pem", type: "spki" })) + .digest("hex"); + + return { privateKey, publicKey, fingerprint }; +} diff --git a/src/util/util/email/index.ts b/src/util/util/email/index.ts index 8966e8f5..d66bbc38 100644 --- a/src/util/util/email/index.ts +++ b/src/util/util/email/index.ts @@ -48,7 +48,6 @@ export const Email: { generateLink: ( type: Omit, id: string, - email: string, ) => Promise; sendMail: ( type: MailTypes, @@ -145,10 +144,9 @@ export const Email: { /** * * @param id user id - * @param email user email */ - generateLink: async function (type, id, email) { - const token = (await generateToken(id, email)) as string; + generateLink: async function (type, id) { + const token = (await generateToken(id)) as string; // puyodead1: this is set to api endpoint because the verification page is on the server since no clients have one, and not all 3rd party clients will have one const instanceUrl = Config.get().api.endpointPublic?.replace("/api", "") || @@ -204,7 +202,7 @@ export const Email: { user, // password change emails don't have links type != MailTypes.changePassword - ? await this.generateLink(type, user.id, email) + ? await this.generateLink(type, user.id) : undefined, ); @@ -213,7 +211,7 @@ export const Email: { user, // password change emails don't have links type != MailTypes.changePassword - ? await this.generateLink(type, user.id, email) + ? await this.generateLink(type, user.id) : undefined, );