-
Spacebar.AdminAPI.TestClient
+
Spacebar.AdminApi.TestClient
diff --git a/extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/Layout/NavMenu.razor.css b/extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/Layout/NavMenu.razor.css
similarity index 100%
rename from extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/Layout/NavMenu.razor.css
rename to extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/Layout/NavMenu.razor.css
diff --git a/extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/Pages/Guilds.razor b/extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/Pages/Guilds.razor
similarity index 98%
rename from extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/Pages/Guilds.razor
rename to extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/Pages/Guilds.razor
index 454808b4e..9a376d24d 100644
--- a/extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/Pages/Guilds.razor
+++ b/extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/Pages/Guilds.razor
@@ -2,7 +2,7 @@
@using System.Net.Http.Headers
@using System.Reflection
@using Spacebar.AdminApi.Models
-@using Spacebar.AdminAPI.TestClient.Services
+@using Spacebar.AdminApi.TestClient.Services
@using ArcaneLibs.Blazor.Components
@using ArcaneLibs.Extensions
@inject Config Config
diff --git a/extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/Pages/Home.razor b/extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/Pages/Home.razor
similarity index 98%
rename from extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/Pages/Home.razor
rename to extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/Pages/Home.razor
index 812a61521..275ca1463 100644
--- a/extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/Pages/Home.razor
+++ b/extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/Pages/Home.razor
@@ -1,6 +1,6 @@
@page "/"
@using System.Net.Http.Headers
-@using Spacebar.AdminAPI.TestClient.Services
+@using Spacebar.AdminApi.TestClient.Services
@inject Config Config
@inject ILocalStorageService LocalStorage
diff --git a/extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/Pages/HttpTestClient.razor b/extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/Pages/HttpTestClient.razor
similarity index 97%
rename from extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/Pages/HttpTestClient.razor
rename to extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/Pages/HttpTestClient.razor
index 0be77a999..b70f4efb6 100644
--- a/extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/Pages/HttpTestClient.razor
+++ b/extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/Pages/HttpTestClient.razor
@@ -1,11 +1,10 @@
@page "/HttpTestClient"
-@using System.Collections.Immutable
@using System.Text.Json
@using ArcaneLibs.Blazor.Components
@using ArcaneLibs.Extensions
-@using Spacebar.AdminAPI.TestClient.Classes.OpenAPI
-@using Spacebar.AdminAPI.TestClient.Pages.HttpTestClientParts
-@using Spacebar.AdminAPI.TestClient.Services
+@using Spacebar.AdminApi.TestClient.Classes.OpenAPI
+@using Spacebar.AdminApi.TestClient.Pages.HttpTestClientParts
+@using Spacebar.AdminApi.TestClient.Services
@inject Config Config
HttpTestClient
diff --git a/extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/Pages/HttpTestClientParts/OpenAPIParameterDescription.razor b/extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/Pages/HttpTestClientParts/OpenAPIParameterDescription.razor
similarity index 91%
rename from extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/Pages/HttpTestClientParts/OpenAPIParameterDescription.razor
rename to extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/Pages/HttpTestClientParts/OpenAPIParameterDescription.razor
index 892224537..8f0667d04 100644
--- a/extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/Pages/HttpTestClientParts/OpenAPIParameterDescription.razor
+++ b/extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/Pages/HttpTestClientParts/OpenAPIParameterDescription.razor
@@ -1,5 +1,5 @@
@using ArcaneLibs.Extensions
-@using Spacebar.AdminAPI.TestClient.Classes.OpenAPI
+@using Spacebar.AdminApi.TestClient.Classes.OpenAPI
@Summary
@if (Parameter.Name != Parameter.Description && !string.IsNullOrWhiteSpace(Parameter.Description)) {
- @Parameter.Description
diff --git a/extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/Pages/Login.razor b/extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/Pages/Login.razor
similarity index 97%
rename from extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/Pages/Login.razor
rename to extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/Pages/Login.razor
index ca1205ed8..196d7336d 100644
--- a/extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/Pages/Login.razor
+++ b/extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/Pages/Login.razor
@@ -1,6 +1,6 @@
@page "/Login"
@using System.Text.Json.Nodes
-@using Spacebar.AdminAPI.TestClient.Services
+@using Spacebar.AdminApi.TestClient.Services
@inject ILocalStorageService LocalStorage
@inject Config Config
@inject NavigationManager Navigation
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
similarity index 100%
rename from extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/Pages/Media/Index.razor
rename to extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/Pages/Media/Index.razor
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
similarity index 98%
rename from extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/Pages/Media/Users.razor
rename to extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/Pages/Media/Users.razor
index 81008a258..f9b43942a 100644
--- a/extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/Pages/Media/Users.razor
+++ b/extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/Pages/Media/Users.razor
@@ -2,7 +2,7 @@
@using System.Net.Http.Headers
@using System.Reflection
@using Spacebar.AdminApi.Models
-@using Spacebar.AdminAPI.TestClient.Services
+@using Spacebar.AdminApi.TestClient.Services
@using ArcaneLibs.Blazor.Components
@inject Config Config
@inject ILocalStorageService LocalStorage
diff --git a/extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/Pages/Users.razor b/extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/Pages/Users.razor
similarity index 98%
rename from extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/Pages/Users.razor
rename to extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/Pages/Users.razor
index c0e678678..b12a5c16d 100644
--- a/extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/Pages/Users.razor
+++ b/extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/Pages/Users.razor
@@ -2,7 +2,7 @@
@using System.Net.Http.Headers
@using System.Reflection
@using Spacebar.AdminApi.Models
-@using Spacebar.AdminAPI.TestClient.Services
+@using Spacebar.AdminApi.TestClient.Services
@using ArcaneLibs.Blazor.Components
@using ArcaneLibs.Extensions
@inject Config Config
diff --git a/extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/Pages/UsersDelete.razor b/extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/Pages/UsersDelete.razor
similarity index 98%
rename from extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/Pages/UsersDelete.razor
rename to extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/Pages/UsersDelete.razor
index 98a3e0fc7..9d5276214 100644
--- a/extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/Pages/UsersDelete.razor
+++ b/extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/Pages/UsersDelete.razor
@@ -4,7 +4,7 @@
@using System.Text.Json.Nodes
@using ArcaneLibs.Extensions
@using Spacebar.AdminApi.Models
-@using Spacebar.AdminAPI.TestClient.Services
+@using Spacebar.AdminApi.TestClient.Services
@inject Config Config
UsersDelete - @Id
diff --git a/extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/Program.cs b/extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/Program.cs
similarity index 95%
rename from extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/Program.cs
rename to extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/Program.cs
index 0e3035a2e..d28066da6 100644
--- a/extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/Program.cs
+++ b/extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/Program.cs
@@ -5,8 +5,8 @@ using System.Text.Json.Serialization;
using Blazored.LocalStorage;
using Microsoft.AspNetCore.Components.Web;
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
-using Spacebar.AdminAPI.TestClient;
-using Spacebar.AdminAPI.TestClient.Services;
+using Spacebar.AdminApi.TestClient;
+using Spacebar.AdminApi.TestClient.Services;
var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.RootComponents.Add
("#app");
diff --git a/extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/Properties/launchSettings.json b/extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/Properties/launchSettings.json
similarity index 100%
rename from extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/Properties/launchSettings.json
rename to extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/Properties/launchSettings.json
diff --git a/extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/Services/Config.cs b/extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/Services/Config.cs
similarity index 92%
rename from extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/Services/Config.cs
rename to extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/Services/Config.cs
index 08fc1fb05..3b045a498 100644
--- a/extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/Services/Config.cs
+++ b/extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/Services/Config.cs
@@ -1,6 +1,6 @@
using System.Text.Json.Serialization;
-namespace Spacebar.AdminAPI.TestClient.Services;
+namespace Spacebar.AdminApi.TestClient.Services;
public class Config {
[JsonPropertyName("api_url")]
diff --git a/extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/Services/StreamingHttpClient.cs b/extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/Services/StreamingHttpClient.cs
similarity index 97%
rename from extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/Services/StreamingHttpClient.cs
rename to extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/Services/StreamingHttpClient.cs
index 67dc673e3..0f2ade713 100644
--- a/extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/Services/StreamingHttpClient.cs
+++ b/extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/Services/StreamingHttpClient.cs
@@ -11,7 +11,7 @@ using System.Text.Json.Serialization;
using ArcaneLibs;
using ArcaneLibs.Extensions;
-namespace Spacebar.AdminAPI.TestClient.Services;
+namespace Spacebar.AdminApi.TestClient.Services;
#if SINGLE_HTTPCLIENT
// TODO: Add URI wrapper for
@@ -83,7 +83,7 @@ public class StreamingHttpClient {
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;
@@ -110,6 +110,7 @@ public class StreamingHttpClient {
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
@@ -148,19 +149,19 @@ public class StreamingHttpClient {
//retry on gateway timeout
// if (responseMessage.StatusCode == HttpStatusCode.GatewayTimeout) {
- // request.ResetSendStatus();
- // return await SendAsync(request, cancellationToken);
+ // 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"
- // };
-
+ // 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();
@@ -293,4 +294,4 @@ public class StreamingHttpClient {
return await SendAsync(request);
}
}
-#endif
+#endif
\ No newline at end of file
diff --git a/extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/Spacebar.AdminAPI.TestClient.csproj b/extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/Spacebar.AdminApi.TestClient.csproj
similarity index 95%
rename from extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/Spacebar.AdminAPI.TestClient.csproj
rename to extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/Spacebar.AdminApi.TestClient.csproj
index fcbd9d61a..bc29a828e 100644
--- a/extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/Spacebar.AdminAPI.TestClient.csproj
+++ b/extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/Spacebar.AdminApi.TestClient.csproj
@@ -14,6 +14,7 @@
false
true
+
@@ -24,8 +25,7 @@
-
-
+
Always
diff --git a/extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/_Imports.razor b/extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/_Imports.razor
similarity index 82%
rename from extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/_Imports.razor
rename to extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/_Imports.razor
index 6cfe3191c..17be595b3 100644
--- a/extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/_Imports.razor
+++ b/extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/_Imports.razor
@@ -7,5 +7,5 @@
@using Microsoft.AspNetCore.Components.Web.Virtualization
@using Microsoft.AspNetCore.Components.WebAssembly.Http
@using Microsoft.JSInterop
-@using Spacebar.AdminAPI.TestClient
-@using Spacebar.AdminAPI.TestClient.Layout
\ No newline at end of file
+@using Spacebar.AdminApi.TestClient
+@using Spacebar.AdminApi.TestClient.Layout
\ No newline at end of file
diff --git a/extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/deps.json b/extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/deps.json
new file mode 100644
index 000000000..c48eef78e
--- /dev/null
+++ b/extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/deps.json
@@ -0,0 +1,217 @@
+[
+ {
+ "pname": "ArcaneLibs",
+ "version": "1.0.0-preview.20251005-232225",
+ "hash": "sha256-EsYLSiyX5Nj+ZpFb6FOcAYqDsQFSbvgm9NKaarJjK/0="
+ },
+ {
+ "pname": "ArcaneLibs.Blazor.Components",
+ "version": "1.0.0-preview.20251005-232225",
+ "hash": "sha256-9rgq/bFNwZf+mpTldzhr7VPwlseLR21nDlJWGgIaGIQ="
+ },
+ {
+ "pname": "Blazored.LocalStorage",
+ "version": "4.5.0",
+ "hash": "sha256-0vklTFHEGgNG8V6ivQictuooyiXS2nMn/qLpfYhEBlE="
+ },
+ {
+ "pname": "Microsoft.AspNetCore.App.Internal.Assets",
+ "version": "10.0.0",
+ "hash": "sha256-IyY5Ymdkmf9S9qRwYXX9rWpzcU3fuDR+ITeaaeJQ/Dk="
+ },
+ {
+ "pname": "Microsoft.AspNetCore.Authorization",
+ "version": "10.0.0",
+ "hash": "sha256-g1MagKFkZF0LttdK5GLdHCXe4d1qOXv57ngz7XnhRrk="
+ },
+ {
+ "pname": "Microsoft.AspNetCore.Components",
+ "version": "10.0.0",
+ "hash": "sha256-nrCuCDRbvD5XQyn3ySW/CD4yKYD6coC71JH2ke6xtXI="
+ },
+ {
+ "pname": "Microsoft.AspNetCore.Components",
+ "version": "9.0.9",
+ "hash": "sha256-1+lIIRfIwHC3XWt2tMuQ3NxoqIsVRgAR+/l9vttQLd8="
+ },
+ {
+ "pname": "Microsoft.AspNetCore.Components.Analyzers",
+ "version": "10.0.0",
+ "hash": "sha256-2mbSRBB/2eT0fYouhDKM5OFRZGQ0Jv8HgcQLvufoHq4="
+ },
+ {
+ "pname": "Microsoft.AspNetCore.Components.Forms",
+ "version": "10.0.0",
+ "hash": "sha256-el2T9pvjNexq5lfJhp+7xZYa/1CS6RchIWKmtiKg+vI="
+ },
+ {
+ "pname": "Microsoft.AspNetCore.Components.Forms",
+ "version": "9.0.9",
+ "hash": "sha256-oUwcqvDLtychAdga+fAJp70wSEdVheYsV6l5Qnfdq6M="
+ },
+ {
+ "pname": "Microsoft.AspNetCore.Components.Web",
+ "version": "10.0.0",
+ "hash": "sha256-xDf1DBNBceAFKu4er2inn6gUKa1T1L8T3ewlFcuxddE="
+ },
+ {
+ "pname": "Microsoft.AspNetCore.Components.Web",
+ "version": "8.0.0",
+ "hash": "sha256-dsCb4B6r5iHPbEp8+uFzAfD1txGI5dEIWgwtT9+GgU8="
+ },
+ {
+ "pname": "Microsoft.AspNetCore.Components.Web",
+ "version": "9.0.9",
+ "hash": "sha256-mgohB7RwOzE5XPB0Lg3h9pUIBKPXaul2F39uxfU7GD8="
+ },
+ {
+ "pname": "Microsoft.AspNetCore.Components.WebAssembly",
+ "version": "10.0.0",
+ "hash": "sha256-YeHralkfEzLmlrBj0jER+ta9hsbXv5skP81178V1ppw="
+ },
+ {
+ "pname": "Microsoft.AspNetCore.Components.WebAssembly.DevServer",
+ "version": "10.0.0",
+ "hash": "sha256-akCQI6iHuKIyDmZh5du+hifzPzQoGeMX3Y7ST/TFxRU="
+ },
+ {
+ "pname": "Microsoft.AspNetCore.Metadata",
+ "version": "10.0.0",
+ "hash": "sha256-TxyiXUx8sWdWFWacBTWFaPeMa3z2+Zmc4VK/Qgq0YRw="
+ },
+ {
+ "pname": "Microsoft.DotNet.HotReload.WebAssembly.Browser",
+ "version": "10.0.100",
+ "hash": "sha256-ppwVl5tBHFFmMZ0EjpKn4OSBYNExf/S6ojeYdefX1+k="
+ },
+ {
+ "pname": "Microsoft.Extensions.Configuration",
+ "version": "10.0.0",
+ "hash": "sha256-MsLskVPpkCvov5+DWIaALCt1qfRRX4u228eHxvpE0dg="
+ },
+ {
+ "pname": "Microsoft.Extensions.Configuration.Abstractions",
+ "version": "10.0.0",
+ "hash": "sha256-GcgrnTAieCV7AVT13zyOjfwwL86e99iiO/MiMOxPGG0="
+ },
+ {
+ "pname": "Microsoft.Extensions.Configuration.Binder",
+ "version": "10.0.0",
+ "hash": "sha256-YSiWoA3VQR22k6+bSEAUqeG7UDzZlJfHWDTubUO5V8U="
+ },
+ {
+ "pname": "Microsoft.Extensions.Configuration.FileExtensions",
+ "version": "10.0.0",
+ "hash": "sha256-rN+3rqrHiTaBfHgP+E4dA8Qm2cFJPfbEcd93yKLsqlQ="
+ },
+ {
+ "pname": "Microsoft.Extensions.Configuration.Json",
+ "version": "10.0.0",
+ "hash": "sha256-VCFukgsxiQ2MFGE6RDMFTGopBHbcZL2t0ER7ENaFXRY="
+ },
+ {
+ "pname": "Microsoft.Extensions.DependencyInjection",
+ "version": "10.0.0",
+ "hash": "sha256-LYm9hVlo/R9c2aAKHsDYJ5vY9U0+3Jvclme3ou3BtvQ="
+ },
+ {
+ "pname": "Microsoft.Extensions.DependencyInjection",
+ "version": "9.0.9",
+ "hash": "sha256-UHG/uj9hjCRWmz2LZ4wR721ooZYGtBy4TT+lNeVzyrU="
+ },
+ {
+ "pname": "Microsoft.Extensions.DependencyInjection.Abstractions",
+ "version": "10.0.0",
+ "hash": "sha256-9iodXP39YqgxomnOPOxd/mzbG0JfOSXzFoNU3omT2Ps="
+ },
+ {
+ "pname": "Microsoft.Extensions.Diagnostics",
+ "version": "10.0.0",
+ "hash": "sha256-o7QkCisEcFIh227qBUfWFci2ns4cgEpLqpX7YvHGToQ="
+ },
+ {
+ "pname": "Microsoft.Extensions.Diagnostics.Abstractions",
+ "version": "10.0.0",
+ "hash": "sha256-cix7QxQ/g3sj6reXu3jn0cRv2RijzceaLLkchEGTt5E="
+ },
+ {
+ "pname": "Microsoft.Extensions.FileProviders.Abstractions",
+ "version": "10.0.0",
+ "hash": "sha256-CHDs2HCN8QcfuYQpgNVszZ5dfXFe4yS9K2GoQXecc20="
+ },
+ {
+ "pname": "Microsoft.Extensions.FileProviders.Physical",
+ "version": "10.0.0",
+ "hash": "sha256-2Rw/cwBO+/A3QY2IjN/c8Y0LhtC1qTBL7VdJiD1J2UQ="
+ },
+ {
+ "pname": "Microsoft.Extensions.FileSystemGlobbing",
+ "version": "10.0.0",
+ "hash": "sha256-ETfVTdsdBtp69EggLg/AARTQW4lLQYVdVldXIQrsjZA="
+ },
+ {
+ "pname": "Microsoft.Extensions.Logging",
+ "version": "10.0.0",
+ "hash": "sha256-P+zPAadLL63k/GqK34/qChqQjY9aIRxZfxlB9lqsSrs="
+ },
+ {
+ "pname": "Microsoft.Extensions.Logging.Abstractions",
+ "version": "10.0.0",
+ "hash": "sha256-BnhgGZc01HwTSxogavq7Ueq4V7iMA3wPnbfRwQ4RhGk="
+ },
+ {
+ "pname": "Microsoft.Extensions.Logging.Configuration",
+ "version": "10.0.0",
+ "hash": "sha256-7/TWO1aq8hdgbcTEKDBWIjgSC9KpFN3kRnMX+12bOkU="
+ },
+ {
+ "pname": "Microsoft.Extensions.Options",
+ "version": "10.0.0",
+ "hash": "sha256-j5MOqZSKeUtxxzmZjzZMGy0vELHdvPraqwTQQQNVsYA="
+ },
+ {
+ "pname": "Microsoft.Extensions.Options.ConfigurationExtensions",
+ "version": "10.0.0",
+ "hash": "sha256-XGAs5DxMvWnmjX8dqRwKY0vsuS40SHvsfJqB1rO4L7k="
+ },
+ {
+ "pname": "Microsoft.Extensions.Primitives",
+ "version": "10.0.0",
+ "hash": "sha256-Dup08KcptLjlnpN5t5//+p4n8FUTgRAq4n/w1s6us+I="
+ },
+ {
+ "pname": "Microsoft.Extensions.Primitives",
+ "version": "9.0.9",
+ "hash": "sha256-bCd4Bj5uP4kT0hCvs0LZS8IVqEtpOIyhSiay5ijJbBA="
+ },
+ {
+ "pname": "Microsoft.Extensions.Validation",
+ "version": "10.0.0",
+ "hash": "sha256-BbmNqKlqNd/37IU5X1wjc3VCxfrBTA1bkVqk1myU+H0="
+ },
+ {
+ "pname": "Microsoft.JSInterop",
+ "version": "10.0.0",
+ "hash": "sha256-/y6la0IzcE5N/thRGLDZYsvNEeWPlW2IwEJtIu3sdaE="
+ },
+ {
+ "pname": "Microsoft.JSInterop",
+ "version": "9.0.9",
+ "hash": "sha256-fCUkGYS6HKZ0NW1xfuI5aCD2yGqpeIjaWFcS0dBZMWE="
+ },
+ {
+ "pname": "Microsoft.JSInterop.WebAssembly",
+ "version": "10.0.0",
+ "hash": "sha256-dJdTHFh4lZOmXI+88mLdJOf6UsKq/80N59+COq/KPpg="
+ },
+ {
+ "pname": "Microsoft.NET.Sdk.WebAssembly.Pack",
+ "version": "10.0.0",
+ "hash": "sha256-AWqn+WUvMjdec4KX4RpTW1ZgT4K+fYhpkrcf706Zt/w="
+ },
+ {
+ "pname": "Microsoft.NETCore.App.Runtime.Mono.browser-wasm",
+ "version": "10.0.0",
+ "hash": "sha256-1CpAq/TBpFaZncLb+Z3xaq/J7/O8ZTEl3qBidCpQj9M="
+ }
+]
diff --git a/extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/wwwroot/appsettings.json b/extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/wwwroot/appsettings.json
similarity index 100%
rename from extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/wwwroot/appsettings.json
rename to extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/wwwroot/appsettings.json
diff --git a/extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/wwwroot/css/app.css b/extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/wwwroot/css/app.css
similarity index 100%
rename from extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/wwwroot/css/app.css
rename to extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/wwwroot/css/app.css
diff --git a/extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/wwwroot/favicon.png b/extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/wwwroot/favicon.png
similarity index 100%
rename from extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/wwwroot/favicon.png
rename to extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/wwwroot/favicon.png
diff --git a/extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/wwwroot/icon-192.png b/extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/wwwroot/icon-192.png
similarity index 100%
rename from extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/wwwroot/icon-192.png
rename to extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/wwwroot/icon-192.png
diff --git a/extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/wwwroot/index.html b/extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/wwwroot/index.html
similarity index 94%
rename from extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/wwwroot/index.html
rename to extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/wwwroot/index.html
index 43c7e50b2..5f8af7c90 100644
--- a/extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/wwwroot/index.html
+++ b/extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/wwwroot/index.html
@@ -9,7 +9,7 @@
-
+
diff --git a/extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.css b/extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.css
similarity index 100%
rename from extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.css
rename to extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.css
diff --git a/extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.css.map b/extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.css.map
similarity index 100%
rename from extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.css.map
rename to extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.css.map
diff --git a/extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.min.css b/extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.min.css
similarity index 100%
rename from extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.min.css
rename to extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.min.css
diff --git a/extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.min.css.map b/extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.min.css.map
similarity index 100%
rename from extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.min.css.map
rename to extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.min.css.map
diff --git a/extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.rtl.css b/extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.rtl.css
similarity index 100%
rename from extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.rtl.css
rename to extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.rtl.css
diff --git a/extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.rtl.css.map b/extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.rtl.css.map
similarity index 100%
rename from extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.rtl.css.map
rename to extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.rtl.css.map
diff --git a/extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.rtl.min.css b/extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.rtl.min.css
similarity index 100%
rename from extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.rtl.min.css
rename to extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.rtl.min.css
diff --git a/extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.rtl.min.css.map b/extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.rtl.min.css.map
similarity index 100%
rename from extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.rtl.min.css.map
rename to extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.rtl.min.css.map
diff --git a/extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.css b/extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.css
similarity index 100%
rename from extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.css
rename to extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.css
diff --git a/extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.css.map b/extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.css.map
similarity index 100%
rename from extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.css.map
rename to extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.css.map
diff --git a/extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.min.css b/extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.min.css
similarity index 100%
rename from extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.min.css
rename to extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.min.css
diff --git a/extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.min.css.map b/extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.min.css.map
similarity index 100%
rename from extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.min.css.map
rename to extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.min.css.map
diff --git a/extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.rtl.css b/extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.rtl.css
similarity index 100%
rename from extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.rtl.css
rename to extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.rtl.css
diff --git a/extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.rtl.css.map b/extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.rtl.css.map
similarity index 100%
rename from extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.rtl.css.map
rename to extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.rtl.css.map
diff --git a/extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.rtl.min.css b/extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.rtl.min.css
similarity index 100%
rename from extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.rtl.min.css
rename to extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.rtl.min.css
diff --git a/extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.rtl.min.css.map b/extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.rtl.min.css.map
similarity index 100%
rename from extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.rtl.min.css.map
rename to extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.rtl.min.css.map
diff --git a/extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.css b/extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.css
similarity index 100%
rename from extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.css
rename to extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.css
diff --git a/extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.css.map b/extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.css.map
similarity index 100%
rename from extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.css.map
rename to extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.css.map
diff --git a/extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.min.css b/extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.min.css
similarity index 100%
rename from extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.min.css
rename to extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.min.css
diff --git a/extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.min.css.map b/extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.min.css.map
similarity index 100%
rename from extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.min.css.map
rename to extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.min.css.map
diff --git a/extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.rtl.css b/extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.rtl.css
similarity index 100%
rename from extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.rtl.css
rename to extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.rtl.css
diff --git a/extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.rtl.css.map b/extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.rtl.css.map
similarity index 100%
rename from extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.rtl.css.map
rename to extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.rtl.css.map
diff --git a/extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.rtl.min.css b/extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.rtl.min.css
similarity index 100%
rename from extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.rtl.min.css
rename to extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.rtl.min.css
diff --git a/extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.rtl.min.css.map b/extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.rtl.min.css.map
similarity index 100%
rename from extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.rtl.min.css.map
rename to extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.rtl.min.css.map
diff --git a/extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/wwwroot/lib/bootstrap/dist/css/bootstrap.css b/extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/wwwroot/lib/bootstrap/dist/css/bootstrap.css
similarity index 100%
rename from extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/wwwroot/lib/bootstrap/dist/css/bootstrap.css
rename to extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/wwwroot/lib/bootstrap/dist/css/bootstrap.css
diff --git a/extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/wwwroot/lib/bootstrap/dist/css/bootstrap.css.map b/extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/wwwroot/lib/bootstrap/dist/css/bootstrap.css.map
similarity index 100%
rename from extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/wwwroot/lib/bootstrap/dist/css/bootstrap.css.map
rename to extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/wwwroot/lib/bootstrap/dist/css/bootstrap.css.map
diff --git a/extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/wwwroot/lib/bootstrap/dist/css/bootstrap.min.css b/extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/wwwroot/lib/bootstrap/dist/css/bootstrap.min.css
similarity index 100%
rename from extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/wwwroot/lib/bootstrap/dist/css/bootstrap.min.css
rename to extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/wwwroot/lib/bootstrap/dist/css/bootstrap.min.css
diff --git a/extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/wwwroot/lib/bootstrap/dist/css/bootstrap.min.css.map b/extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/wwwroot/lib/bootstrap/dist/css/bootstrap.min.css.map
similarity index 100%
rename from extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/wwwroot/lib/bootstrap/dist/css/bootstrap.min.css.map
rename to extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/wwwroot/lib/bootstrap/dist/css/bootstrap.min.css.map
diff --git a/extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/wwwroot/lib/bootstrap/dist/css/bootstrap.rtl.css b/extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/wwwroot/lib/bootstrap/dist/css/bootstrap.rtl.css
similarity index 100%
rename from extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/wwwroot/lib/bootstrap/dist/css/bootstrap.rtl.css
rename to extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/wwwroot/lib/bootstrap/dist/css/bootstrap.rtl.css
diff --git a/extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/wwwroot/lib/bootstrap/dist/css/bootstrap.rtl.css.map b/extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/wwwroot/lib/bootstrap/dist/css/bootstrap.rtl.css.map
similarity index 100%
rename from extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/wwwroot/lib/bootstrap/dist/css/bootstrap.rtl.css.map
rename to extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/wwwroot/lib/bootstrap/dist/css/bootstrap.rtl.css.map
diff --git a/extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/wwwroot/lib/bootstrap/dist/css/bootstrap.rtl.min.css b/extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/wwwroot/lib/bootstrap/dist/css/bootstrap.rtl.min.css
similarity index 100%
rename from extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/wwwroot/lib/bootstrap/dist/css/bootstrap.rtl.min.css
rename to extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/wwwroot/lib/bootstrap/dist/css/bootstrap.rtl.min.css
diff --git a/extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/wwwroot/lib/bootstrap/dist/css/bootstrap.rtl.min.css.map b/extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/wwwroot/lib/bootstrap/dist/css/bootstrap.rtl.min.css.map
similarity index 100%
rename from extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/wwwroot/lib/bootstrap/dist/css/bootstrap.rtl.min.css.map
rename to extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/wwwroot/lib/bootstrap/dist/css/bootstrap.rtl.min.css.map
diff --git a/extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/wwwroot/lib/bootstrap/dist/js/bootstrap.bundle.js b/extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/wwwroot/lib/bootstrap/dist/js/bootstrap.bundle.js
similarity index 100%
rename from extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/wwwroot/lib/bootstrap/dist/js/bootstrap.bundle.js
rename to extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/wwwroot/lib/bootstrap/dist/js/bootstrap.bundle.js
diff --git a/extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/wwwroot/lib/bootstrap/dist/js/bootstrap.bundle.js.map b/extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/wwwroot/lib/bootstrap/dist/js/bootstrap.bundle.js.map
similarity index 100%
rename from extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/wwwroot/lib/bootstrap/dist/js/bootstrap.bundle.js.map
rename to extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/wwwroot/lib/bootstrap/dist/js/bootstrap.bundle.js.map
diff --git a/extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/wwwroot/lib/bootstrap/dist/js/bootstrap.bundle.min.js b/extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/wwwroot/lib/bootstrap/dist/js/bootstrap.bundle.min.js
similarity index 100%
rename from extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/wwwroot/lib/bootstrap/dist/js/bootstrap.bundle.min.js
rename to extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/wwwroot/lib/bootstrap/dist/js/bootstrap.bundle.min.js
diff --git a/extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/wwwroot/lib/bootstrap/dist/js/bootstrap.bundle.min.js.map b/extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/wwwroot/lib/bootstrap/dist/js/bootstrap.bundle.min.js.map
similarity index 100%
rename from extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/wwwroot/lib/bootstrap/dist/js/bootstrap.bundle.min.js.map
rename to extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/wwwroot/lib/bootstrap/dist/js/bootstrap.bundle.min.js.map
diff --git a/extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/wwwroot/lib/bootstrap/dist/js/bootstrap.esm.js b/extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/wwwroot/lib/bootstrap/dist/js/bootstrap.esm.js
similarity index 100%
rename from extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/wwwroot/lib/bootstrap/dist/js/bootstrap.esm.js
rename to extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/wwwroot/lib/bootstrap/dist/js/bootstrap.esm.js
diff --git a/extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/wwwroot/lib/bootstrap/dist/js/bootstrap.esm.js.map b/extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/wwwroot/lib/bootstrap/dist/js/bootstrap.esm.js.map
similarity index 100%
rename from extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/wwwroot/lib/bootstrap/dist/js/bootstrap.esm.js.map
rename to extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/wwwroot/lib/bootstrap/dist/js/bootstrap.esm.js.map
diff --git a/extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/wwwroot/lib/bootstrap/dist/js/bootstrap.esm.min.js b/extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/wwwroot/lib/bootstrap/dist/js/bootstrap.esm.min.js
similarity index 100%
rename from extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/wwwroot/lib/bootstrap/dist/js/bootstrap.esm.min.js
rename to extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/wwwroot/lib/bootstrap/dist/js/bootstrap.esm.min.js
diff --git a/extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/wwwroot/lib/bootstrap/dist/js/bootstrap.esm.min.js.map b/extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/wwwroot/lib/bootstrap/dist/js/bootstrap.esm.min.js.map
similarity index 100%
rename from extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/wwwroot/lib/bootstrap/dist/js/bootstrap.esm.min.js.map
rename to extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/wwwroot/lib/bootstrap/dist/js/bootstrap.esm.min.js.map
diff --git a/extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/wwwroot/lib/bootstrap/dist/js/bootstrap.js b/extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/wwwroot/lib/bootstrap/dist/js/bootstrap.js
similarity index 100%
rename from extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/wwwroot/lib/bootstrap/dist/js/bootstrap.js
rename to extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/wwwroot/lib/bootstrap/dist/js/bootstrap.js
diff --git a/extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/wwwroot/lib/bootstrap/dist/js/bootstrap.js.map b/extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/wwwroot/lib/bootstrap/dist/js/bootstrap.js.map
similarity index 100%
rename from extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/wwwroot/lib/bootstrap/dist/js/bootstrap.js.map
rename to extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/wwwroot/lib/bootstrap/dist/js/bootstrap.js.map
diff --git a/extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/wwwroot/lib/bootstrap/dist/js/bootstrap.min.js b/extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/wwwroot/lib/bootstrap/dist/js/bootstrap.min.js
similarity index 100%
rename from extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/wwwroot/lib/bootstrap/dist/js/bootstrap.min.js
rename to extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/wwwroot/lib/bootstrap/dist/js/bootstrap.min.js
diff --git a/extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/wwwroot/lib/bootstrap/dist/js/bootstrap.min.js.map b/extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/wwwroot/lib/bootstrap/dist/js/bootstrap.min.js.map
similarity index 100%
rename from extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/wwwroot/lib/bootstrap/dist/js/bootstrap.min.js.map
rename to extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/wwwroot/lib/bootstrap/dist/js/bootstrap.min.js.map
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
similarity index 100%
rename from extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/wwwroot/lib/jetbrains-mono/jetbrains-mono.css
rename to extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/wwwroot/lib/jetbrains-mono/jetbrains-mono.css
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
similarity index 100%
rename from extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/wwwroot/lib/jetbrains-mono/webfonts/JetBrainsMono-Bold.woff2
rename to extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/wwwroot/lib/jetbrains-mono/webfonts/JetBrainsMono-Bold.woff2
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
similarity index 100%
rename from extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/wwwroot/lib/jetbrains-mono/webfonts/JetBrainsMono-BoldItalic.woff2
rename to extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/wwwroot/lib/jetbrains-mono/webfonts/JetBrainsMono-BoldItalic.woff2
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
similarity index 100%
rename from extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/wwwroot/lib/jetbrains-mono/webfonts/JetBrainsMono-ExtraBold.woff2
rename to extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/wwwroot/lib/jetbrains-mono/webfonts/JetBrainsMono-ExtraBold.woff2
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
similarity index 100%
rename from extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/wwwroot/lib/jetbrains-mono/webfonts/JetBrainsMono-ExtraBoldItalic.woff2
rename to extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/wwwroot/lib/jetbrains-mono/webfonts/JetBrainsMono-ExtraBoldItalic.woff2
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
similarity index 100%
rename from extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/wwwroot/lib/jetbrains-mono/webfonts/JetBrainsMono-ExtraLight.woff2
rename to extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/wwwroot/lib/jetbrains-mono/webfonts/JetBrainsMono-ExtraLight.woff2
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
similarity index 100%
rename from extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/wwwroot/lib/jetbrains-mono/webfonts/JetBrainsMono-ExtraLightItalic.woff2
rename to extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/wwwroot/lib/jetbrains-mono/webfonts/JetBrainsMono-ExtraLightItalic.woff2
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
similarity index 100%
rename from extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/wwwroot/lib/jetbrains-mono/webfonts/JetBrainsMono-Italic.woff2
rename to extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/wwwroot/lib/jetbrains-mono/webfonts/JetBrainsMono-Italic.woff2
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
similarity index 100%
rename from extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/wwwroot/lib/jetbrains-mono/webfonts/JetBrainsMono-Light.woff2
rename to extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/wwwroot/lib/jetbrains-mono/webfonts/JetBrainsMono-Light.woff2
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
similarity index 100%
rename from extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/wwwroot/lib/jetbrains-mono/webfonts/JetBrainsMono-LightItalic.woff2
rename to extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/wwwroot/lib/jetbrains-mono/webfonts/JetBrainsMono-LightItalic.woff2
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
similarity index 100%
rename from extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/wwwroot/lib/jetbrains-mono/webfonts/JetBrainsMono-Medium.woff2
rename to extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/wwwroot/lib/jetbrains-mono/webfonts/JetBrainsMono-Medium.woff2
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
similarity index 100%
rename from extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/wwwroot/lib/jetbrains-mono/webfonts/JetBrainsMono-MediumItalic.woff2
rename to extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/wwwroot/lib/jetbrains-mono/webfonts/JetBrainsMono-MediumItalic.woff2
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
similarity index 100%
rename from extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/wwwroot/lib/jetbrains-mono/webfonts/JetBrainsMono-Regular.woff2
rename to extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/wwwroot/lib/jetbrains-mono/webfonts/JetBrainsMono-Regular.woff2
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
similarity index 100%
rename from extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/wwwroot/lib/jetbrains-mono/webfonts/JetBrainsMono-SemiBold.woff2
rename to extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/wwwroot/lib/jetbrains-mono/webfonts/JetBrainsMono-SemiBold.woff2
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
similarity index 100%
rename from extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/wwwroot/lib/jetbrains-mono/webfonts/JetBrainsMono-SemiBoldItalic.woff2
rename to extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/wwwroot/lib/jetbrains-mono/webfonts/JetBrainsMono-SemiBoldItalic.woff2
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
similarity index 100%
rename from extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/wwwroot/lib/jetbrains-mono/webfonts/JetBrainsMono-Thin.woff2
rename to extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/wwwroot/lib/jetbrains-mono/webfonts/JetBrainsMono-Thin.woff2
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
similarity index 100%
rename from extra/admin-api/Utilities/Spacebar.AdminAPI.TestClient/wwwroot/lib/jetbrains-mono/webfonts/JetBrainsMono-ThinItalic.woff2
rename to extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/wwwroot/lib/jetbrains-mono/webfonts/JetBrainsMono-ThinItalic.woff2
diff --git a/extra/admin-api/Utilities/Spacebar.AdminAPITest/Program.cs b/extra/admin-api/Utilities/Spacebar.AdminApiTest/Program.cs
similarity index 100%
rename from extra/admin-api/Utilities/Spacebar.AdminAPITest/Program.cs
rename to extra/admin-api/Utilities/Spacebar.AdminApiTest/Program.cs
diff --git a/extra/admin-api/Utilities/Spacebar.AdminAPITest/Spacebar.AdminAPITest.csproj b/extra/admin-api/Utilities/Spacebar.AdminApiTest/Spacebar.AdminApiTest.csproj
similarity index 67%
rename from extra/admin-api/Utilities/Spacebar.AdminAPITest/Spacebar.AdminAPITest.csproj
rename to extra/admin-api/Utilities/Spacebar.AdminApiTest/Spacebar.AdminApiTest.csproj
index ec27f0b60..5daabdd0a 100644
--- a/extra/admin-api/Utilities/Spacebar.AdminAPITest/Spacebar.AdminAPITest.csproj
+++ b/extra/admin-api/Utilities/Spacebar.AdminApiTest/Spacebar.AdminApiTest.csproj
@@ -10,9 +10,7 @@
-
- ..\..\..\..\..\..\..\.nuget\packages\arcanelibs\1.0.0-preview.20241210-161342\lib\net10.0\ArcaneLibs.dll
-
+
diff --git a/extra/admin-api/db-patches/00-use-stream-class.patch b/extra/admin-api/db-patches/00-use-stream-class.patch
new file mode 100644
index 000000000..1636f1202
--- /dev/null
+++ b/extra/admin-api/db-patches/00-use-stream-class.patch
@@ -0,0 +1,14 @@
+diff --git a/Spacebar.Db/Contexts/SpacebarDbContext.cs b/Spacebar.Db/Contexts/SpacebarDbContext.cs
+index 0a707c75..7d380b1c 100644
+--- a/Spacebar.Db/Contexts/SpacebarDbContext.cs
++++ b/Spacebar.Db/Contexts/SpacebarDbContext.cs
+@@ -1,7 +1,6 @@
+-using System;
+-using System.Collections.Generic;
+-using Microsoft.EntityFrameworkCore;
++using Microsoft.EntityFrameworkCore;
+ using Spacebar.Db.Models;
++using Stream = Spacebar.Db.Models.Stream;
+
+ namespace Spacebar.Db.Contexts;
+
diff --git a/extra/admin-api/db-patches/01-ulong-user-rights.patch b/extra/admin-api/db-patches/01-ulong-user-rights.patch
new file mode 100644
index 000000000..362b67d7d
--- /dev/null
+++ b/extra/admin-api/db-patches/01-ulong-user-rights.patch
@@ -0,0 +1,28 @@
+diff --git a/Spacebar.Db/Models/User.cs b/Spacebar.Db/Models/User.cs
+index 7af60bf4..323846d4 100644
+--- a/Spacebar.Db/Models/User.cs
++++ b/Spacebar.Db/Models/User.cs
+@@ -90,19 +90,19 @@ public partial class User
+ public string? Email { get; set; }
+
+ [Column("flags")]
+- public long Flags { get; set; }
++ public ulong Flags { get; set; }
+
+ [Column("public_flags")]
+- public long PublicFlags { get; set; }
++ public ulong PublicFlags { get; set; }
+
+ [Column("purchased_flags")]
+- public long PurchasedFlags { get; set; }
++ public ulong PurchasedFlags { get; set; }
+
+ [Column("premium_usage_flags")]
+ public int PremiumUsageFlags { get; set; }
+
+ [Column("rights")]
+- public long Rights { get; set; }
++ public ulong Rights { get; set; }
+
+ [Column("data")]
+ public string Data { get; set; } = null!;
diff --git a/extra/admin-api/db-patches/db-00-fix-flags.patch b/extra/admin-api/db-patches/db-00-fix-flags.patch
deleted file mode 100644
index 437368f49..000000000
--- a/extra/admin-api/db-patches/db-00-fix-flags.patch
+++ /dev/null
@@ -1,13 +0,0 @@
---- Spacebar.Db/Models/User.cs.orig 2025-10-05 22:04:37.168566856 +0200
-+++ Spacebar.Db/Models/User.cs 2025-10-05 22:07:11.519980808 +0200
-@@ -92,8 +92,8 @@
- [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.lock b/extra/admin-api/flake.lock
index 664ccdaa6..10a741433 100644
Binary files a/extra/admin-api/flake.lock and b/extra/admin-api/flake.lock differ
diff --git a/extra/admin-api/flake.nix b/extra/admin-api/flake.nix
index 6cf356827..7626bf6cf 100644
--- a/extra/admin-api/flake.nix
+++ b/extra/admin-api/flake.nix
@@ -1,5 +1,5 @@
{
- description = "Spacebar Admin API, written in C#.";
+ description = "Spacebar server, written in Typescript.";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
@@ -12,81 +12,7 @@
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
- ];
- };
- }
- );
+ import ./outputs.nix {
+ inherit self nixpkgs flake-utils;
+ };
}
diff --git a/extra/admin-api/nuget.config b/extra/admin-api/nuget.config
new file mode 100644
index 000000000..aa87aca25
--- /dev/null
+++ b/extra/admin-api/nuget.config
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
diff --git a/extra/admin-api/outputs.nix b/extra/admin-api/outputs.nix
new file mode 100644
index 000000000..ccbed17d9
--- /dev/null
+++ b/extra/admin-api/outputs.nix
@@ -0,0 +1,128 @@
+{
+ self,
+ nixpkgs,
+ flake-utils,
+}:
+let
+ rVersion =
+ let
+ rev = self.sourceInfo.shortRev or self.sourceInfo.dirtyShortRev;
+ date = builtins.substring 0 8 self.sourceInfo.lastModifiedDate;
+ time = builtins.substring 8 6 self.sourceInfo.lastModifiedDate;
+ in
+ "preview.${date}-${time}"; #+${rev}";
+in
+flake-utils.lib.eachSystem flake-utils.lib.allSystems (
+ system:
+ let
+ pkgs = import nixpkgs {
+ inherit system;
+ };
+ lib = pkgs.lib;
+ makeNupkg =
+ {
+ name,
+ nugetDeps ? null,
+ projectReferences ? [ ],
+ projectFile ? "${name}/${name}.csproj",
+ runtimeId ? null,
+ useAppHost ? null,
+ packNupkg ? true,
+ }@args:
+ pkgs.buildDotnetModule rec {
+ inherit
+ projectReferences
+ nugetDeps
+ projectFile
+ runtimeId
+ useAppHost
+ ;
+
+ pname = "${name}";
+ version = "1.0.0-" + rVersion;
+ dotnetPackFlags = [
+ "--include-symbols"
+ "--include-source"
+ "--version-suffix ${rVersion}"
+ ];
+ # dotnetFlags = [ "-v:diag" ];
+ dotnet-sdk = pkgs.dotnet-sdk_10;
+ dotnet-runtime = pkgs.dotnet-aspnetcore_10;
+ src = pkgs.lib.cleanSource ./.;
+ packNupkg = true;
+ meta = with pkgs.lib; {
+ description = "Spacebar Server, Typescript Edition (C# extensions)";
+ homepage = "https://github.com/spacebarchat/server";
+ license = licenses.agpl3Plus;
+ maintainers = with maintainers; [ RorySys ];
+ };
+ };
+ in
+ {
+ packages =
+ let
+ proj = self.packages.${system};
+ in
+ {
+ Spacebar-Db = makeNupkg {
+ name = "Spacebar.Db";
+ nugetDeps = Spacebar.Db/deps.json;
+ };
+ Spacebar-AdminApi-Models = makeNupkg {
+ name = "Spacebar.AdminApi.Models";
+ };
+ Spacebar-ConfigModel = makeNupkg {
+ name = "Spacebar.ConfigModel";
+ };
+ Spacebar-CleanSettingsRows = makeNupkg {
+ name = "Spacebar.CleanSettingsRows";
+ nugetDeps = Spacebar.CleanSettingsRows/deps.json;
+ packNupkg = false;
+ projectReferences = [ proj.Spacebar-Db ];
+ };
+ Spacebar-AdminApi = makeNupkg {
+ name = "Spacebar.AdminApi";
+ nugetDeps = Spacebar.AdminApi/deps.json;
+ packNupkg = false;
+ projectReferences = [
+ proj.Spacebar-AdminApi-Models
+ proj.Spacebar-Db
+ ];
+
+ };
+# Spacebar-AdminApi-TestClient = makeNupkg {
+# name = "Spacebar.AdminApi.TestClient";
+# projectFile = "Utilities/Spacebar.AdminApi.TestClient/Spacebar.AdminApi.TestClient.csproj";
+# nugetDeps = Utilities/Spacebar.AdminApi.TestClient/deps.json;
+# projectReferences = [
+# proj.Spacebar-AdminApi-Models
+# ];
+## runtimeId = "browser-wasm";
+## useAppHost = false;
+# };
+ };
+
+ # containers.docker = pkgs.dockerTools.buildLayeredImage {
+ # name = "spacebar-server-ts";
+ # tag = builtins.replaceStrings [ "+" ] [ "_" ] self.packages.${system}.default.version;
+ # contents = [ self.packages.${system}.default ];
+ # config = {
+ # Cmd = [ "${self.outputs.packages.${system}.default}/bin/start-bundle" ];
+ # Expose = [ "3001" ];
+ # };
+ # };
+ }
+)
+// {
+ # nixosModules.default = import ./nix/modules/default self;
+ checks =
+ let
+ pkgs = import nixpkgs { system = "x86_64-linux"; };
+ in
+ pkgs.lib.recursiveUpdate (pkgs.lib.attrsets.unionOfDisjoint { } self.packages) {
+ x86_64-linux = {
+ # spacebar-server-tests = self.packages.x86_64-linux.default.passthru.tests;
+ # docker-image = self.containers.x86_64-linux.docker;
+ };
+ };
+}
\ No newline at end of file
diff --git a/extra/admin-api/scaffold-db b/extra/admin-api/scaffold-db
index 0f332e005..19d321306 100755
--- a/extra/admin-api/scaffold-db
+++ b/extra/admin-api/scaffold-db
@@ -19,6 +19,8 @@ rm Class1.cs
dotnet add package Npgsql.EntityFrameworkCore.PostgreSQL -n -f net9.0
dotnet add package Microsoft.EntityFrameworkCore.Design -n -f net9.0
+dotnet restore
+
dotnet-ef dbcontext scaffold "Host=127.0.0.1; Username=postgres; Database=sb-server-scaffold" \
Npgsql.EntityFrameworkCore.PostgreSQL \
-o Models \
@@ -28,6 +30,8 @@ dotnet-ef dbcontext scaffold "Host=127.0.0.1; Username=postgres; Database=sb-ser
--no-onconfiguring \
--data-annotations
-for patch in db-patches/*.patch; do
- patch -p3 < $patch
-done
\ No newline at end of file
+for patch in ../db-patches/*.patch; do
+ patch -p1 < $patch
+done
+
+echo 'Scaffolded database and applied patches. Dont forget to generate new patches with `git diff --relative Spacebar.Db/path/xyz > db-patches/001-your-patch-name.patch!`'
\ No newline at end of file
diff --git a/flake.nix b/flake.nix
index dd947375a..805ac7886 100644
--- a/flake.nix
+++ b/flake.nix
@@ -12,143 +12,154 @@
nixpkgs,
flake-utils,
}:
- let
- hashesFile = builtins.fromJSON (builtins.readFile ./hashes.json);
- rVersion =
+ nixpkgs.lib.recursiveUpdate
+ (
let
- rev = self.sourceInfo.shortRev or self.sourceInfo.dirtyShortRev;
- date = builtins.substring 0 8 self.sourceInfo.lastModifiedDate;
- time = builtins.substring 8 6 self.sourceInfo.lastModifiedDate;
+ hashesFile = builtins.fromJSON (builtins.readFile ./hashes.json);
+ rVersion =
+ let
+ rev = self.sourceInfo.shortRev or self.sourceInfo.dirtyShortRev;
+ date = builtins.substring 0 8 self.sourceInfo.lastModifiedDate;
+ time = builtins.substring 8 6 self.sourceInfo.lastModifiedDate;
+ in
+ "preview.${date}-${time}"; # +${rev}";
in
- "preview.${date}-${time}+${rev}";
- in
- flake-utils.lib.eachSystem flake-utils.lib.allSystems (
- system:
- let
- pkgs = import nixpkgs {
- inherit system;
- };
- lib = pkgs.lib;
- in
- {
- packages = {
- default = pkgs.buildNpmPackage {
- pname = "spacebar-server-ts";
- nodejs = pkgs.nodejs_24;
- version = "1.0.0-" + rVersion;
+ flake-utils.lib.eachSystem flake-utils.lib.allSystems (
+ system:
+ let
+ pkgs = import nixpkgs {
+ inherit system;
+ };
+ lib = pkgs.lib;
+ in
+ {
+ packages = {
+ default = pkgs.buildNpmPackage {
+ pname = "spacebar-server-ts";
+ nodejs = pkgs.nodejs_24;
+ version = "1.0.0-" + rVersion;
- 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";
- maintainers = with maintainers; [ RorySys ]; # lol.
+ 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";
+ maintainers = with maintainers; [ RorySys ]; # lol.
+ };
+
+ src = ./.;
+ npmDepsHash = hashesFile.npmDepsHash;
+ npmBuildScript = "build:src";
+ makeCacheWritable = true;
+ nativeBuildInputs = with pkgs; [
+ python3
+ ];
+ installPhase =
+ let
+ revsFile = pkgs.writeText "spacebar-server-rev.json" (
+ builtins.toJSON {
+ rev = self.sourceInfo.rev or self.sourceInfo.dirtyRev;
+ shortRev = self.sourceInfo.shortRev or self.sourceInfo.dirtyShortRev;
+ lastModified = self.sourceInfo.lastModified;
+ }
+ );
+ in
+ ''
+ 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[@]}"
+ ${./nix/trimNodeModules.sh}
+
+ # Copy outputs
+ echo "Installing package into $out"
+ mkdir -p $out
+ cp -r assets dist node_modules package.json $out/
+ cp ${revsFile} $out/.rev
+
+ # Create wrappers for start scripts
+ echo "Creating wrappers for start scripts"
+ for i in dist/**/start.js
+ do
+ makeWrapper ${pkgs.nodejs_24}/bin/node $out/bin/start-`dirname ''${i/dist\//}` --prefix NODE_PATH : $out/node_modules --add-flags --enable-source-maps --add-flags $out/$i
+ done
+
+ # set +x
+ runHook postInstall
+ '';
+
+ passthru.tests = pkgs.testers.runNixOSTest (import ./nix/tests/test-bundle-starts.nix self);
+ };
+
+ update-nix-hashes = pkgs.writeShellApplication {
+ name = "update-nix";
+ runtimeInputs = with pkgs; [
+ prefetch-npm-deps
+ nix
+ jq
+ ];
+ text = ''
+ rm -rf node_modules
+ ${pkgs.nodejs_24}/bin/npm install --save --no-audit --no-fund --prefer-offline
+ DEPS_HASH=$(prefetch-npm-deps package-lock.json)
+ TMPFILE=$(mktemp)
+ jq '.npmDepsHash = "'"$DEPS_HASH"'"' hashes.json > "$TMPFILE"
+ mv -- "$TMPFILE" hashes.json
+ '';
+ };
+
+ update-nix-flake = pkgs.writeShellApplication {
+ name = "update-nix";
+ runtimeInputs = with pkgs; [
+ prefetch-npm-deps
+ nix
+ jq
+ ];
+ text = ''
+ nix flake update --extra-experimental-features 'nix-command flakes'
+ '';
+ };
};
- src = ./.;
- npmDepsHash = hashesFile.npmDepsHash;
- npmBuildScript = "build:src";
- makeCacheWritable = true;
- nativeBuildInputs = with pkgs; [
- python3
- ];
- installPhase =
+ containers.docker = pkgs.dockerTools.buildLayeredImage {
+ name = "spacebar-server-ts";
+ tag = builtins.replaceStrings [ "+" ] [ "_" ] self.packages.${system}.default.version;
+ contents = [ self.packages.${system}.default ];
+ config = {
+ Cmd = [ "${self.outputs.packages.${system}.default}/bin/start-bundle" ];
+ Expose = [ "3001" ];
+ };
+ };
+
+ devShells.default = pkgs.mkShell {
+ buildInputs = with pkgs; [
+ nodejs_24
+ nodePackages.typescript
+ nodePackages.patch-package
+ nodePackages.prettier
+ ];
+ };
+ }
+ )
+ // {
+ nixosModules.default = import ./nix/modules/default self;
+ checks =
let
- revsFile = pkgs.writeText "spacebar-server-rev.json" (builtins.toJSON {
- rev = self.sourceInfo.rev or self.sourceInfo.dirtyRev;
- shortRev = self.sourceInfo.shortRev or self.sourceInfo.dirtyShortRev;
- lastModified = self.sourceInfo.lastModified;
- });
- in ''
- 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[@]}"
- ${./nix/trimNodeModules.sh}
-
- # Copy outputs
- echo "Installing package into $out"
- mkdir -p $out
- cp -r assets dist node_modules package.json $out/
- cp ${revsFile} $out/.rev
-
- # Create wrappers for start scripts
- echo "Creating wrappers for start scripts"
- for i in dist/**/start.js
- do
- makeWrapper ${pkgs.nodejs_24}/bin/node $out/bin/start-`dirname ''${i/dist\//}` --prefix NODE_PATH : $out/node_modules --add-flags --enable-source-maps --add-flags $out/$i
- done
-
- # set +x
- runHook postInstall
- '';
-
- passthru.tests = pkgs.testers.runNixOSTest (import ./nix/tests/test-bundle-starts.nix self);
- };
-
- update-nix-hashes = pkgs.writeShellApplication {
- name = "update-nix";
- runtimeInputs = with pkgs; [
- prefetch-npm-deps
- nix
- jq
- ];
- text = ''
- rm -rf node_modules
- ${pkgs.nodejs_24}/bin/npm install --save --no-audit --no-fund --prefer-offline
- DEPS_HASH=$(prefetch-npm-deps package-lock.json)
- TMPFILE=$(mktemp)
- jq '.npmDepsHash = "'"$DEPS_HASH"'"' hashes.json > "$TMPFILE"
- mv -- "$TMPFILE" hashes.json
- '';
- };
-
- update-nix-flake = pkgs.writeShellApplication {
- name = "update-nix";
- runtimeInputs = with pkgs; [
- prefetch-npm-deps
- nix
- jq
- ];
- text = ''
- nix flake update --extra-experimental-features 'nix-command flakes'
- '';
- };
- };
-
- containers.docker = pkgs.dockerTools.buildLayeredImage {
- name = "spacebar-server-ts";
- tag = builtins.replaceStrings [ "+" ] [ "_" ] self.packages.${system}.default.version;
- contents = [ self.packages.${system}.default ];
- config = {
- Cmd = [ "${self.outputs.packages.${system}.default}/bin/start-bundle" ];
- Expose = [ "3001" ];
- };
- };
-
- devShells.default = pkgs.mkShell {
- buildInputs = with pkgs; [
- nodejs_24
- nodePackages.typescript
- nodePackages.patch-package
- nodePackages.prettier
- ];
- };
- }
- )
- // {
- nixosModules.default = import ./nix/modules/default self;
- checks =
- let
- pkgs = import nixpkgs { system = "x86_64-linux"; };
- in
- pkgs.lib.recursiveUpdate (pkgs.lib.attrsets.unionOfDisjoint { } self.packages) {
- x86_64-linux = {
- spacebar-server-tests = self.packages.x86_64-linux.default.passthru.tests;
- docker-image = self.containers.x86_64-linux.docker;
- };
- };
- };
-}
+ pkgs = import nixpkgs { system = "x86_64-linux"; };
+ in
+ pkgs.lib.recursiveUpdate (pkgs.lib.attrsets.unionOfDisjoint { } self.packages) {
+ x86_64-linux = {
+ spacebar-server-tests = self.packages.x86_64-linux.default.passthru.tests;
+ docker-image = self.containers.x86_64-linux.docker;
+ };
+ };
+ }
+ )
+ (
+ import ./extra/admin-api/outputs.nix {
+ inherit self nixpkgs flake-utils;
+ }
+ );
+}
\ No newline at end of file
diff --git a/nix/tests/test-bundle-starts.nix b/nix/tests/test-bundle-starts.nix
index 4378369cc..14108a411 100644
--- a/nix/tests/test-bundle-starts.nix
+++ b/nix/tests/test-bundle-starts.nix
@@ -7,7 +7,14 @@ self:
nodes.machine = {
imports = [ self.nixosModules.default ];
- services.spacebarchat-server.enable = true;
+ services.spacebarchat-server = {
+ enable = true;
+ settings = {
+ api = { endpointPublic = "http://localhost:3001/api/v9/"; };
+ cdn = { endpointPublic = "http://localhost:3001/"; endpointPrivate = "http://localhost:3001/"; };
+ gateway = { endpointPublic = "ws://localhost:3001/"; };
+ };
+ };
};
testScript = ''
diff --git a/scripts/test.js b/scripts/test.js
index 69e9fdd64..c79e336ff 100644
--- a/scripts/test.js
+++ b/scripts/test.js
@@ -23,10 +23,21 @@
const { spawn } = require("child_process");
const path = require("path");
+const fs = require("fs");
-const server = spawn("node", [
- path.join(__dirname, "..", "dist", "bundle", "start.js"),
-]);
+const cfgFile = path.join(__dirname, "test_config.json");
+process.env.CONFIG_PATH = cfgFile;
+
+fs.writeFileSync(
+ cfgFile,
+ JSON.stringify({
+ api: { endpointPublic: "http://localhost:3001/api/v9/" },
+ cdn: { endpointPublic: "http://localhost:3001/", endpointPrivate: "http://localhost:3001/" },
+ gateway: { endpointPublic: "ws://localhost:3001/" },
+ }),
+);
+
+const server = spawn("node", [path.join(__dirname, "..", "dist", "bundle", "start.js")]);
server.stdout.on("data", (data) => {
process.stdout.write(data);
diff --git a/scripts/util/getRouteDescriptions.js b/scripts/util/getRouteDescriptions.js
index 9f329f323..0372aae7e 100644
--- a/scripts/util/getRouteDescriptions.js
+++ b/scripts/util/getRouteDescriptions.js
@@ -49,7 +49,7 @@ function proxy(file, apiMethod, apiPathPrefix, apiPath, ...args) {
const opts = args.find((x) => x?.prototype?.OPTS_MARKER == true);
if (!opts)
return console.error(
- ` \x1b[5m${bgRedBright("ERROR")}\x1b[25m ${file.replace(path.resolve(__dirname, "..", "..", "dist"), "/src/")} has route without route() description middleware: ${colorizeMethod(apiMethod)} ${formatPath(apiPath)}`,
+ ` \x1b[5m${bgRedBright("ERROR")}\x1b[25m ${file.replace(path.resolve(__dirname, "..", "..", "dist"), "/src")} has route without route() description middleware: ${colorizeMethod(apiMethod)} ${formatPath(apiPath)}`,
);
console.log(`${colorizeMethod(apiMethod).padStart("DELETE".length + 10)} ${formatPath(apiPathPrefix + apiPath)}`);
diff --git a/src/api/middlewares/Authentication.ts b/src/api/middlewares/Authentication.ts
index 4bb809a67..2dd992ee3 100644
--- a/src/api/middlewares/Authentication.ts
+++ b/src/api/middlewares/Authentication.ts
@@ -70,6 +70,7 @@ declare global {
user_bot: boolean;
token: { id: string; iat: number };
rights: Rights;
+ fingerprint?: string;
}
}
}
@@ -77,6 +78,15 @@ declare global {
export async function Authentication(req: Request, res: Response, next: NextFunction) {
if (req.method === "OPTIONS") return res.sendStatus(204);
const url = req.url.replace(API_PREFIX, "");
+
+ if (req.headers.cookie?.split("; ").find((x) => x.startsWith("__sb_sessid=")))
+ req.fingerprint = req.headers.cookie
+ .split("; ")
+ .find((x) => x.startsWith("__sb_sessid="))!
+ .split("=")[1];
+ // for some reason we need to require here, else the openapi generator fails with "route is not a function"
+ else res.setHeader("Set-Cookie", `__sb_sessid=${req.fingerprint = (await require("../util")).randomString(32)}; Secure; HttpOnly; SameSite=None; Path=/`);
+
if (
NO_AUTHORIZATION_ROUTES.some((x) => {
if (typeof x !== "string") {
@@ -102,10 +112,14 @@ export async function Authentication(req: Request, res: Response, next: NextFunc
})
)
return next();
+
if (!req.headers.authorization) return next(new HTTPError("Missing Authorization Header", 401));
try {
- const { decoded, user } = await checkToken(req.headers.authorization);
+ const { decoded, user } = await checkToken(req.headers.authorization, {
+ ipAddress: req.ip,
+ fingerprint: req.fingerprint,
+ });
req.token = decoded;
req.user_id = decoded.id;
diff --git a/src/api/middlewares/CORS.ts b/src/api/middlewares/CORS.ts
index ca6abb82d..34532d3f5 100644
--- a/src/api/middlewares/CORS.ts
+++ b/src/api/middlewares/CORS.ts
@@ -21,20 +21,16 @@ import { NextFunction, Request, Response } from "express";
// TODO: config settings
export function CORS(req: Request, res: Response, next: NextFunction) {
- res.set("Access-Control-Allow-Origin", "*");
+ res.set("Access-Control-Allow-Credentials", "true");
+ res.set("Access-Control-Allow-Headers", req.header("Access-Control-Request-Headers") || "*");
+ res.set("Access-Control-Allow-Methods", req.header("Access-Control-Request-Method") || "*");
+ res.set("Access-Control-Allow-Origin", req.header("Origin") ?? "*");
+ res.set("Access-Control-Max-Age", "5"); // dont make it too long so we can change it dynamically
// TODO: use better CSP
res.set(
"Content-security-policy",
"default-src * data: blob: filesystem: about: ws: wss: 'unsafe-inline' 'unsafe-eval'; script-src * data: blob: 'unsafe-inline' 'unsafe-eval'; connect-src * data: blob: 'unsafe-inline'; img-src * data: blob: 'unsafe-inline'; frame-src * data: blob: ; style-src * data: blob: 'unsafe-inline'; font-src * data: blob: 'unsafe-inline';",
);
- res.set(
- "Access-Control-Allow-Headers",
- req.header("Access-Control-Request-Headers") || "*",
- );
- res.set(
- "Access-Control-Allow-Methods",
- req.header("Access-Control-Request-Methods") || "*",
- );
if (req.method === "OPTIONS") {
res.status(204).end();
diff --git a/src/api/middlewares/RateLimit.ts b/src/api/middlewares/RateLimit.ts
index 81ccbc515..1a41ad4f6 100644
--- a/src/api/middlewares/RateLimit.ts
+++ b/src/api/middlewares/RateLimit.ts
@@ -16,7 +16,6 @@
along with this program. If not, see .
*/
-import { getIpAdress } from "@spacebar/api";
import { Config, getRights, listenEvent } from "@spacebar/util";
import { NextFunction, Request, Response, Router } from "express";
import { API_PREFIX_TRAILING_SLASH } from "./Authentication";
@@ -65,21 +64,14 @@ export default function rateLimit(opts: {
if (rights.has("BYPASS_RATE_LIMITS")) return next();
}
- const bucket_id =
- opts.bucket ||
- req.originalUrl.replace(API_PREFIX_TRAILING_SLASH, "");
- let executor_id = getIpAdress(req);
+ const bucket_id = opts.bucket || req.originalUrl.replace(API_PREFIX_TRAILING_SLASH, "");
+ let executor_id = req.ip || "127.0.0.1";
if (!opts.onlyIp && req.user_id) executor_id = req.user_id;
let max_hits = opts.count;
if (opts.bot && req.user_bot) max_hits = opts.bot;
- if (opts.GET && ["GET", "OPTIONS", "HEAD"].includes(req.method))
- max_hits = opts.GET;
- else if (
- opts.MODIFY &&
- ["POST", "DELETE", "PATCH", "PUT"].includes(req.method)
- )
- max_hits = opts.MODIFY;
+ if (opts.GET && ["GET", "OPTIONS", "HEAD"].includes(req.method)) max_hits = opts.GET;
+ else if (opts.MODIFY && ["POST", "DELETE", "PATCH", "PUT"].includes(req.method)) max_hits = opts.MODIFY;
const offender = Cache.get(executor_id + bucket_id);
@@ -104,18 +96,13 @@ export default function rateLimit(opts: {
}
res.set("X-RateLimit-Reset", `${reset}`);
- res.set(
- "X-RateLimit-Reset-After",
- `${Math.max(0, Math.ceil(resetAfterSec))}`,
- );
+ res.set("X-RateLimit-Reset-After", `${Math.max(0, Math.ceil(resetAfterSec))}`);
if (offender.blocked) {
const global = bucket_id === "global";
// each block violation pushes the expiry one full window further
reset += opts.window * 1000;
- offender.expires_at = new Date(
- offender.expires_at.getTime() + opts.window * 1000,
- );
+ offender.expires_at = new Date(offender.expires_at.getTime() + opts.window * 1000);
resetAfterMs = reset - Date.now();
resetAfterSec = Math.ceil(resetAfterMs / 1000);
@@ -129,10 +116,7 @@ export default function rateLimit(opts: {
res
.status(429)
.set("X-RateLimit-Remaining", "0")
- .set(
- "Retry-After",
- `${Math.max(0, Math.ceil(resetAfterSec))}`,
- )
+ .set("Retry-After", `${Math.max(0, Math.ceil(resetAfterSec))}`)
// TODO: error rate limit message translation
.send({
message: "You are being rate limited.",
@@ -156,11 +140,7 @@ export default function rateLimit(opts: {
// check if error and increment error rate limit
if (res.statusCode >= 400 && opts.error) {
return hitRoute(hitRouteOpts);
- } else if (
- res.statusCode >= 200 &&
- res.statusCode < 300 &&
- opts.success
- ) {
+ } else if (res.statusCode >= 200 && res.statusCode < 300 && opts.success) {
return hitRoute(hitRouteOpts);
}
});
@@ -213,18 +193,10 @@ export async function initRateLimits(app: Router) {
app.use("/webhooks/:webhook_id", rateLimit(routes.webhook));
app.use("/channels/:channel_id", rateLimit(routes.channel));
app.use("/auth/login", rateLimit(routes.auth.login));
- app.use(
- "/auth/register",
- rateLimit({ onlyIp: true, success: true, ...routes.auth.register }),
- );
+ app.use("/auth/register", rateLimit({ onlyIp: true, success: true, ...routes.auth.register }));
}
-async function hitRoute(opts: {
- executor_id: string;
- bucket_id: string;
- max_hits: number;
- window: number;
-}) {
+async function hitRoute(opts: { executor_id: string; bucket_id: string; max_hits: number; window: number }) {
const id = opts.executor_id + opts.bucket_id;
let limit = Cache.get(id);
if (!limit) {
diff --git a/src/api/routes/auth/forgot.ts b/src/api/routes/auth/forgot.ts
index 12bc9cb07..6e613820f 100644
--- a/src/api/routes/auth/forgot.ts
+++ b/src/api/routes/auth/forgot.ts
@@ -16,10 +16,10 @@
along with this program. If not, see .
*/
-import { getIpAdress, route, verifyCaptcha } from "@spacebar/api";
+import { route, verifyCaptcha } from "@spacebar/api";
import { Config, Email, User } from "@spacebar/util";
import { Request, Response, Router } from "express";
-import { ForgotPasswordSchema } from "@spacebar/schemas"
+import { ForgotPasswordSchema } from "@spacebar/schemas";
const router = Router({ mergeParams: true });
router.post(
@@ -38,10 +38,7 @@ router.post(
const config = Config.get();
- if (
- config.passwordReset.requireCaptcha &&
- config.security.captcha.enabled
- ) {
+ if (config.passwordReset.requireCaptcha && config.security.captcha.enabled) {
const { sitekey, service } = config.security.captcha;
if (!captcha_key) {
return res.status(400).json({
@@ -51,7 +48,7 @@ router.post(
});
}
- const ip = getIpAdress(req);
+ const ip = req.ip;
const verify = await verifyCaptcha(captcha_key, ip);
if (!verify.success) {
return res.status(400).json({
@@ -71,9 +68,7 @@ router.post(
if (user && user.email) {
Email.sendResetPassword(user, user.email).catch((e) => {
- console.error(
- `Failed to send password reset email to ${user.username}#${user.discriminator} (${user.id}): ${e}`,
- );
+ console.error(`Failed to send password reset email to ${user.username}#${user.discriminator} (${user.id}): ${e}`);
});
}
},
diff --git a/src/api/routes/auth/location-metadata.ts b/src/api/routes/auth/location-metadata.ts
index a08c98abe..92f47909d 100644
--- a/src/api/routes/auth/location-metadata.ts
+++ b/src/api/routes/auth/location-metadata.ts
@@ -16,7 +16,7 @@
along with this program. If not, see .
*/
-import { getIpAdress, route } from "@spacebar/api";
+import { route } from "@spacebar/api";
import { IpDataClient } from "@spacebar/util";
import { Request, Response, Router } from "express";
const router = Router({ mergeParams: true });
@@ -33,7 +33,7 @@ router.get(
async (req: Request, res: Response) => {
//TODO
//Note: It's most likely related to legal. At the moment Discord hasn't finished this too
- const country_code = (await IpDataClient.getIpInfo(getIpAdress(req)))?.country_code;
+ const country_code = (await IpDataClient.getIpInfo(req.ip!))?.country_code;
res.json({
consent_required: false,
country_code: country_code,
diff --git a/src/api/routes/auth/login.ts b/src/api/routes/auth/login.ts
index 06e3fe43e..c8ef62731 100644
--- a/src/api/routes/auth/login.ts
+++ b/src/api/routes/auth/login.ts
@@ -16,19 +16,12 @@
along with this program. If not, see .
*/
-import { getIpAdress, route, verifyCaptcha } from "@spacebar/api";
-import {
- Config,
- FieldErrors,
- User,
- WebAuthn,
- generateToken,
- generateWebAuthnTicket,
-} from "@spacebar/util";
+import { route, verifyCaptcha } from "@spacebar/api";
+import { Config, FieldErrors, User, WebAuthn, generateToken, generateWebAuthnTicket } from "@spacebar/util";
import bcrypt from "bcrypt";
import crypto from "crypto";
import { Request, Response, Router } from "express";
-import { LoginSchema } from "@spacebar/schemas"
+import { LoginSchema } from "@spacebar/schemas";
const router: Router = Router({ mergeParams: true });
export default router;
@@ -61,7 +54,7 @@ router.post(
});
}
- const ip = getIpAdress(req);
+ const ip = req.ip;
const verify = await verifyCaptcha(captcha_key, ip);
if (!verify.success) {
return res.status(400).json({
@@ -74,17 +67,7 @@ router.post(
const user = await User.findOneOrFail({
where: [{ phone: login }, { email: login }],
- select: [
- "data",
- "id",
- "disabled",
- "deleted",
- "totp_secret",
- "mfa_enabled",
- "webauthn_enabled",
- "security_keys",
- "verified",
- ],
+ select: ["data", "id", "disabled", "deleted", "totp_secret", "mfa_enabled", "webauthn_enabled", "security_keys", "verified"],
relations: ["security_keys", "settings"],
}).catch(() => {
throw FieldErrors({
@@ -100,10 +83,7 @@ router.post(
});
// the salt is saved in the password refer to bcrypt docs
- const same_password = await bcrypt.compare(
- password,
- user.data.hash || "",
- );
+ const same_password = await bcrypt.compare(password, user.data.hash || "");
if (!same_password) {
throw FieldErrors({
login: {
@@ -122,8 +102,7 @@ router.post(
throw FieldErrors({
login: {
code: "ACCOUNT_LOGIN_VERIFICATION_EMAIL",
- message:
- "Email verification is required, please check your email.",
+ message: "Email verification is required, please check your email.",
},
});
}
@@ -152,9 +131,7 @@ router.post(
const challenge = JSON.stringify({
publicKey: {
...options,
- challenge: Buffer.from(options.challenge).toString(
- "base64",
- ),
+ challenge: Buffer.from(options.challenge).toString("base64"),
allowCredentials: user.security_keys.map((x) => ({
id: x.key_id,
type: "public-key",
@@ -178,10 +155,8 @@ router.post(
if (undelete) {
// undelete refers to un'disable' here
- if (user.disabled)
- await User.update({ id: user.id }, { disabled: false });
- if (user.deleted)
- await User.update({ id: user.id }, { deleted: false });
+ if (user.disabled) await User.update({ id: user.id }, { disabled: false });
+ if (user.deleted) await User.update({ id: user.id }, { deleted: false });
} else {
if (user.deleted)
return res.status(400).json({
diff --git a/src/api/routes/auth/register.ts b/src/api/routes/auth/register.ts
index 418f0a529..d4b061be2 100644
--- a/src/api/routes/auth/register.ts
+++ b/src/api/routes/auth/register.ts
@@ -16,7 +16,7 @@
along with this program. If not, see .
*/
-import { getIpAdress, route, verifyCaptcha } from "@spacebar/api";
+import { route, verifyCaptcha } from "@spacebar/api";
import { Config, FieldErrors, Invite, User, ValidRegistrationToken, generateToken, IpDataClient, AbuseIpDbClient } from "@spacebar/util";
import bcrypt from "bcrypt";
import { Request, Response, Router } from "express";
@@ -38,7 +38,7 @@ router.post(
async (req: Request, res: Response) => {
const body = req.body as RegisterSchema;
const { register, security, limits } = Config.get();
- const ip = getIpAdress(req);
+ const ip = req.ip!;
// Reg tokens
// They're a one time use token that bypasses registration limits ( rates, disabled reg, etc )
@@ -143,7 +143,7 @@ router.post(
const ipData = await IpDataClient.getIpInfo(ip);
if (ipData) {
- if(!ipData.threat) {
+ if (!ipData.threat) {
console.log("Invalid IPData.co response, missing threat field", ipData);
}
const categories = Object.entries(ipData.threat)
@@ -287,7 +287,7 @@ router.post(
},
})) >= limits.absoluteRate.register.limit
) {
- console.log(`Global register ratelimit exceeded for ${getIpAdress(req)}, ${req.body.username}, ${req.body.invite || "No invite given"}`);
+ console.log(`Global register ratelimit exceeded for ${req.ip}, ${req.body.username}, ${req.body.invite || "No invite given"}`);
throw FieldErrors({
email: {
code: "TOO_MANY_REGISTRATIONS",
diff --git a/src/api/routes/auth/reset.ts b/src/api/routes/auth/reset.ts
index b14eb3791..5cb5690d3 100644
--- a/src/api/routes/auth/reset.ts
+++ b/src/api/routes/auth/reset.ts
@@ -51,6 +51,8 @@ router.post(
try {
const userTokenData = await checkToken(token, {
select: ["email"],
+ fingerprint: req.fingerprint,
+ ipAddress: req.ip
});
user = userTokenData.user;
} catch {
diff --git a/src/api/routes/auth/verify/index.ts b/src/api/routes/auth/verify/index.ts
index b85120d84..8c636196e 100644
--- a/src/api/routes/auth/verify/index.ts
+++ b/src/api/routes/auth/verify/index.ts
@@ -16,14 +16,8 @@
along with this program. If not, see .
*/
-import { getIpAdress, route, verifyCaptcha } from "@spacebar/api";
-import {
- checkToken,
- Config,
- FieldErrors,
- generateToken,
- User,
-} from "@spacebar/util";
+import { route, verifyCaptcha } from "@spacebar/api";
+import { checkToken, Config, FieldErrors, generateToken, User } from "@spacebar/util";
import { Request, Response, Router } from "express";
const router = Router({ mergeParams: true });
@@ -67,7 +61,7 @@ router.post(
});
}
- const ip = getIpAdress(req);
+ const ip = req.ip;
const verify = await verifyCaptcha(captcha_key, ip);
if (!verify.success) {
return res.status(400).json({
@@ -81,7 +75,10 @@ router.post(
let user;
try {
- const userTokenData = await checkToken(token);
+ const userTokenData = await checkToken(token, {
+ fingerprint: req.fingerprint,
+ ipAddress: req.ip
+ });
user = userTokenData.user;
} catch {
throw FieldErrors({
diff --git a/src/api/routes/channels/#channel_id/messages/index.ts b/src/api/routes/channels/#channel_id/messages/index.ts
index 8790241d8..43c4984e0 100644
--- a/src/api/routes/channels/#channel_id/messages/index.ts
+++ b/src/api/routes/channels/#channel_id/messages/index.ts
@@ -548,7 +548,7 @@ router.delete(
// TODO: handle other read state types
if (body.read_state_type != ReadStateType.CHANNEL) return res.status(204).send();
- const readState = await ReadState.findOne({where: {channel_id}});
+ const readState = await ReadState.findOne({ where: { channel_id, user_id: req.user_id } });
if (readState) {
await readState.remove();
}
diff --git a/src/api/routes/guilds/#guild_id/bans.ts b/src/api/routes/guilds/#guild_id/bans.ts
index 1274c195d..39eee34aa 100644
--- a/src/api/routes/guilds/#guild_id/bans.ts
+++ b/src/api/routes/guilds/#guild_id/bans.ts
@@ -16,11 +16,11 @@
along with this program. If not, see .
*/
-import { getIpAdress, route } from "@spacebar/api";
+import { route } from "@spacebar/api";
import { Ban, DiscordApiErrors, GuildBanAddEvent, GuildBanRemoveEvent, Member, User, emitEvent } from "@spacebar/util";
import { Request, Response, Router } from "express";
import { HTTPError } from "lambert-server";
-import { APIBansArray, BanRegistrySchema, GuildBansResponse } from "@spacebar/schemas";
+import { APIBansArray, BanCreateSchema, BanRegistrySchema, GuildBansResponse } from "@spacebar/schemas";
const router: Router = Router({ mergeParams: true });
@@ -198,6 +198,15 @@ router.put(
async (req: Request, res: Response) => {
const { guild_id } = req.params;
const banned_user_id = req.params.user_id;
+ const opts = req.body as BanCreateSchema;
+
+ let deleteMessagesMs = opts.delete_message_days
+ ? (opts.delete_message_days as number) * 86400000
+ : opts.delete_message_seconds
+ ? (opts.delete_message_seconds as number) * 1000
+ : 0;
+
+ if (deleteMessagesMs < 0) deleteMessagesMs = 0;
if (req.user_id === banned_user_id && banned_user_id === req.permission?.cache.guild?.owner_id)
throw new HTTPError("You are the guild owner, hence can't ban yourself", 403);
@@ -207,6 +216,7 @@ router.put(
const existingBan = await Ban.findOne({
where: { guild_id: guild_id, user_id: banned_user_id },
});
+
// Bans on already banned users are silently ignored
if (existingBan) return res.status(204).send();
@@ -227,6 +237,7 @@ router.put(
data: {
guild_id: guild_id,
user: banned_user.toPublicUser(),
+ delete_message_secs: Math.floor(deleteMessagesMs / 1000),
},
guild_id: guild_id,
} as GuildBanAddEvent),
@@ -279,4 +290,4 @@ router.delete(
},
);
-export default router;
+export default router;
\ No newline at end of file
diff --git a/src/api/routes/guilds/#guild_id/bulk-ban.ts b/src/api/routes/guilds/#guild_id/bulk-ban.ts
index 3d3ed1c39..a564e2d48 100644
--- a/src/api/routes/guilds/#guild_id/bulk-ban.ts
+++ b/src/api/routes/guilds/#guild_id/bulk-ban.ts
@@ -16,7 +16,7 @@
along with this program. If not, see .
*/
-import { getIpAdress, route } from "@spacebar/api";
+import { route } from "@spacebar/api";
import { Ban, DiscordApiErrors, GuildBanAddEvent, Member, User, emitEvent } from "@spacebar/util";
import { Request, Response, Router } from "express";
import { HTTPError } from "lambert-server";
@@ -81,7 +81,7 @@ router.post(
const ban = Ban.create({
user_id: banned_user_id,
guild_id: guild_id,
- ip: getIpAdress(req),
+ ip: req.ip,
executor_id: req.user_id,
reason: req.body.reason, // || otherwise empty
});
diff --git a/src/api/routes/guilds/#guild_id/regions.ts b/src/api/routes/guilds/#guild_id/regions.ts
index a7fad818d..a3c5a81b6 100644
--- a/src/api/routes/guilds/#guild_id/regions.ts
+++ b/src/api/routes/guilds/#guild_id/regions.ts
@@ -16,7 +16,7 @@
along with this program. If not, see .
*/
-import { getIpAdress, getVoiceRegions, route } from "@spacebar/api";
+import { getVoiceRegions, route } from "@spacebar/api";
import { Guild } from "@spacebar/util";
import { Request, Response, Router } from "express";
@@ -38,12 +38,7 @@ router.get(
const { guild_id } = req.params;
const guild = await Guild.findOneOrFail({ where: { id: guild_id } });
//TODO we should use an enum for guild's features and not hardcoded strings
- return res.json(
- await getVoiceRegions(
- getIpAdress(req),
- guild.features.includes("VIP_REGIONS"),
- ),
- );
+ return res.json(await getVoiceRegions(req.ip!, guild.features.includes("VIP_REGIONS")));
},
);
diff --git a/src/api/routes/invites/index.ts b/src/api/routes/invites/index.ts
index c77d36eaa..87715f35e 100644
--- a/src/api/routes/invites/index.ts
+++ b/src/api/routes/invites/index.ts
@@ -16,7 +16,7 @@
along with this program. If not, see .
*/
-import { getIpAdress, route } from "@spacebar/api";
+import { route } from "@spacebar/api";
import { Ban, DiscordApiErrors, emitEvent, getPermission, Guild, Invite, InviteDeleteEvent, PublicInviteRelation, User } from "@spacebar/util";
import { Request, Response, Router } from "express";
import { HTTPError } from "lambert-server";
@@ -83,7 +83,7 @@ router.post(
const ban = await Ban.findOne({
where: [
{ guild_id: guild_id, user_id: req.user_id },
- { guild_id: guild_id, ip: getIpAdress(req) },
+ { guild_id: guild_id, ip: req.ip },
],
});
diff --git a/src/api/routes/oauth2/authorize.ts b/src/api/routes/oauth2/authorize.ts
index a14d57057..4084a059c 100644
--- a/src/api/routes/oauth2/authorize.ts
+++ b/src/api/routes/oauth2/authorize.ts
@@ -17,18 +17,9 @@
*/
import { route } from "@spacebar/api";
-import {
- ApiError,
- Application,
- DiscordApiErrors,
- FieldErrors,
- Member,
- Permissions,
- User,
- getPermission,
-} from "@spacebar/util";
+import { ApiError, Application, DiscordApiErrors, FieldErrors, Member, Permissions, User, getPermission } from "@spacebar/util";
import { Request, Response, Router } from "express";
-import { ApplicationAuthorizeSchema } from "@spacebar/schemas"
+import { ApplicationAuthorizeSchema } from "@spacebar/schemas";
const router = Router({ mergeParams: true });
// TODO: scopes, other oauth types
@@ -84,26 +75,18 @@ router.get(
id: req.user_id,
bot: false,
},
- select: [
- "id",
- "username",
- "avatar",
- "discriminator",
- "public_flags",
- ],
+ select: ["id", "username", "avatar", "discriminator", "public_flags"],
});
const guilds = await Member.find({
where: {
- user: {
- id: req.user_id,
- },
+ id: req.user_id,
},
relations: ["guild", "roles", "user"],
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
//@ts-ignore
// prettier-ignore
- select: ["guild.id", "guild.name", "guild.icon", "guild.mfa_level", "guild.owner_id", "roles.id", "communication_disabled_until", "user.flags"],
+ select: ["guild.id", "guild.name", "guild.icon", "guild.mfa_level", "guild.owner_id", "roles.id", "user.flags"],
});
const guildsWithPermissions = guilds.map((x) => {
@@ -112,7 +95,7 @@ router.get(
id: user.id,
roles: x.roles?.map((x) => x.id) || [],
communication_disabled_until: x.communication_disabled_until,
- flags: x.user.flags
+ flags: x.user.flags,
},
guild: {
roles: x?.roles || [],
@@ -211,18 +194,9 @@ router.post(
// TODO: captcha verification
// TODO: MFA verification
- const perms = await getPermission(
- req.user_id,
- body.guild_id,
- undefined,
- { member_relations: ["user"] },
- );
+ const perms = await getPermission(req.user_id, body.guild_id, undefined, { member_relations: ["user"] });
// getPermission cache won't exist if we're owner
- if (
- Object.keys(perms.cache || {}).length > 0 &&
- perms.cache.member?.user.bot
- )
- throw DiscordApiErrors.UNAUTHORIZED;
+ if (Object.keys(perms.cache || {}).length > 0 && perms.cache.member?.user.bot) throw DiscordApiErrors.UNAUTHORIZED;
perms.hasThrow("MANAGE_GUILD");
const app = await Application.findOne({
@@ -235,12 +209,7 @@ router.post(
// TODO: use DiscordApiErrors
// findOneOrFail throws code 404
if (!app) throw new ApiError("Unknown Application", 10002, 404);
- if (!app.bot)
- throw new ApiError(
- "OAuth2 application does not have a bot",
- 50010,
- 400,
- );
+ if (!app.bot) throw new ApiError("OAuth2 application does not have a bot", 50010, 400);
await Member.addToGuild(app.id, body.guild_id);
diff --git a/src/api/routes/users/#user_id/delete.ts b/src/api/routes/users/#user_id/delete.ts
index dbb18e290..50494a238 100644
--- a/src/api/routes/users/#user_id/delete.ts
+++ b/src/api/routes/users/#user_id/delete.ts
@@ -17,9 +17,25 @@
*/
import { route } from "@spacebar/api";
-import { emitEvent, Member, User, UserDeleteEvent } from "@spacebar/util";
+import {
+ Channel,
+ ChannelDeleteEvent,
+ ChannelRecipientRemoveEvent,
+ emitEvent,
+ Emoji,
+ Guild,
+ InstanceBan,
+ Member,
+ Recipient,
+ Sticker,
+ Stopwatch,
+ User,
+ UserDeleteEvent,
+ UserSettingsProtos,
+} from "@spacebar/util";
import { Request, Response, Router } from "express";
-import { PrivateUserProjection } from "@spacebar/schemas";
+import { ChannelType, InstanceUserDeleteSchema, PrivateUserProjection } from "@spacebar/schemas";
+import { Not } from "typeorm";
const router = Router({ mergeParams: true });
@@ -27,6 +43,7 @@ router.post(
"/",
route({
right: "MANAGE_USERS",
+ requestBody: "InstanceUserDeleteSchema",
responses: {
204: {},
403: {
@@ -38,12 +55,132 @@ router.post(
},
}),
async (req: Request, res: Response) => {
- await User.findOneOrFail({
+ const sw = Stopwatch.startNew();
+ const body = req.body as InstanceUserDeleteSchema | undefined;
+ const user = await User.findOneOrFail({
where: { id: req.params.user_id },
select: [...PrivateUserProjection, "data"],
});
+
+ if (!(await InstanceBan.findOne({ where: { user_id: user.id } })))
+ await InstanceBan.create({ user_id: user.id, reason: body?.reason ?? "" }).save();
+
+ // prevent bugginess with clients - delete all DMs, only having half of the conversation is quite useless anyhow
+ const dmChannels = await user.getDmChannels();
+ for (const channel of dmChannels) {
+ console.log(`[Instance ban] Deleting DM channel ${channel.id} for user ${user.id}`);
+ await emitEvent({
+ event: "CHANNEL_DELETE",
+ data: channel.toJSON(),
+ channel_id: channel.id,
+ } as ChannelDeleteEvent);
+ await Recipient.delete({ channel_id: channel.id });
+ await Channel.deleteChannel(channel);
+ }
+
+ //leave all group channels
+ const groupChannels = await Channel.find({
+ where: { type: ChannelType.GROUP_DM },
+ relations: ["recipients"],
+ select: {
+ id: true,
+ owner_id: true,
+ recipients: {
+ id: true,
+ user_id: true,
+ },
+ },
+ });
+
+ await Promise.all(
+ groupChannels.map(async (channel) => {
+ const recipient = channel.recipients!.find((r) => r.user_id === user.id);
+ if (recipient) {
+ await Recipient.delete({ id: recipient.id });
+ await emitEvent({
+ event: "CHANNEL_RECIPIENT_REMOVE",
+ data: {
+ user: user.toPublicUser(),
+ channel_id: channel.id,
+ },
+ channel_id: channel.id,
+ } as ChannelRecipientRemoveEvent);
+ console.log(`[Instance ban] Removed user ${user.id} from group channel ${channel.id}`);
+ }
+
+ // if no recipients remain, delete the channel
+ const remainingRecipients = await Recipient.find({ where: { channel_id: channel.id } });
+ if (remainingRecipients.length === 0) {
+ await emitEvent({
+ event: "CHANNEL_DELETE",
+ data: channel.toJSON(),
+ channel_id: channel.id,
+ } as ChannelDeleteEvent);
+ await Channel.deleteChannel(channel);
+ console.log(`[Instance ban] Deleted empty group channel ${channel.id}`);
+ } else {
+ // otherwise, if the banned user was the owner, reassign ownership
+ if (channel.owner_id === user.id) {
+ channel.owner_id = remainingRecipients[0].user_id;
+ await channel.save();
+ console.log(`[Instance ban] Reassigned ownership of group channel ${channel.id} to user ${channel.owner_id}`);
+ }
+ }
+ }),
+ );
+
+ // change ownership on guilds
+ const guilds = await Guild.find({ where: { owner_id: req.params.user_id } });
+ await Promise.all(
+ guilds.map(async (guild) => {
+ const members = await Member.find({
+ where: { guild_id: guild.id, id: Not(req.params.user_id) },
+ relations: { roles: true },
+ select: { id: true, roles: { id: true, position: true } },
+ });
+ const sortedMembers = members
+ .filter((m) => m.id !== req.params.user_id)
+ .sort((a, b) => {
+ const aHighestRole = a.roles.reduce((prev, curr) => (curr.position > prev.position ? curr : prev), { position: -1 } as { position: number });
+ const bHighestRole = b.roles.reduce((prev, curr) => (curr.position > prev.position ? curr : prev), { position: -1 } as { position: number });
+ return bHighestRole.position - aHighestRole.position;
+ });
+ if (sortedMembers.length === 0) {
+ // no members left, delete guild
+ await guild.remove();
+ console.log(`[Instance ban] Deleted guild ${guild.id} as user ${user.id} was the last member`);
+ } else {
+ // assign new owner
+ guild.owner_id = sortedMembers[0].id;
+ await guild.save();
+ console.log(`[Instance ban] Transferred ownership of guild ${guild.id} to user ${guild.owner_id}`);
+
+ // safety - reassign emojis/stickers owned by the old owner
+ const stickers = await Sticker.find({ where: { guild_id: guild.id, user_id: req.params.user_id } });
+ await Promise.all(
+ stickers.map(async (sticker) => {
+ sticker.user_id = guild.owner_id;
+ await sticker.save();
+ console.log(`[Instance ban] Reassigned sticker ${sticker.id} ownership to user ${guild.owner_id}`);
+ }),
+ );
+
+ const emojis = await Emoji.find({ where: { guild_id: guild.id, user_id: req.params.user_id } });
+ await Promise.all(
+ emojis.map(async (emoji) => {
+ emoji.user_id = guild.owner_id!;
+ await emoji.save();
+ console.log(`[Instance ban] Reassigned emoji ${emoji.id} ownership to user ${guild.owner_id}`);
+ }),
+ );
+ }
+ }),
+ );
+
const members = await Member.find({ where: { id: req.params.user_id } });
- await Promise.all([...members.map((member) => Member.removeFromGuild(member.id, member.guild_id)), User.delete({ id: req.params.user_id })]);
+ await Promise.all([...members.map((member) => Member.removeFromGuild(member.id, member.guild_id))]);
+ await UserSettingsProtos.delete({ user_id: req.params.user_id });
+ await User.delete({ id: req.params.user_id });
// TODO: respect intents as USER_DELETE has potential to cause privacy issues
await emitEvent({
@@ -52,6 +189,7 @@ router.post(
data: { user_id: req.params.user_id },
} as UserDeleteEvent);
+ console.log(`[Instance ban] Deleted user ${user.id} from instance in ${sw.elapsed().toString()}`);
res.sendStatus(204);
},
);
diff --git a/src/api/routes/voice/regions.ts b/src/api/routes/voice/regions.ts
index 9fdd6fdef..ba2612316 100644
--- a/src/api/routes/voice/regions.ts
+++ b/src/api/routes/voice/regions.ts
@@ -16,7 +16,7 @@
along with this program. If not, see .
*/
-import { getIpAdress, getVoiceRegions, route } from "@spacebar/api";
+import { getVoiceRegions, route } from "@spacebar/api";
import { Request, Response, Router } from "express";
const router: Router = Router({ mergeParams: true });
@@ -31,7 +31,7 @@ router.get(
},
}),
async (req: Request, res: Response) => {
- res.json(await getVoiceRegions(getIpAdress(req), true)); //vip true?
+ res.json(await getVoiceRegions(req.ip!, true)); //vip true?
},
);
diff --git a/src/api/util/utility/ipAddress.ts b/src/api/util/utility/ipAddress.ts
index 914999a71..19408253d 100644
--- a/src/api/util/utility/ipAddress.ts
+++ b/src/api/util/utility/ipAddress.ts
@@ -16,41 +16,16 @@
along with this program. If not, see .
*/
-import { Config } from "@spacebar/util";
-import { Request } from "express";
-
-export function getIpAdress(req: Request): string {
- // TODO: express can do this (trustProxies: true)?
-
- return req.ip!;
-}
-
type Location = { latitude: number; longitude: number };
-export function distanceBetweenLocations(
- loc1: Location,
- loc2: Location,
-): number {
- return distanceBetweenCoords(
- loc1.latitude,
- loc1.longitude,
- loc2.latitude,
- loc2.longitude,
- );
+export function distanceBetweenLocations(loc1: Location, loc2: Location): number {
+ return distanceBetweenCoords(loc1.latitude, loc1.longitude, loc2.latitude, loc2.longitude);
}
//Haversine function
-function distanceBetweenCoords(
- lat1: number,
- lon1: number,
- lat2: number,
- lon2: number,
-) {
+function distanceBetweenCoords(lat1: number, lon1: number, lat2: number, lon2: number) {
const p = 0.017453292519943295; // Math.PI / 180
const c = Math.cos;
- const a =
- 0.5 -
- c((lat2 - lat1) * p) / 2 +
- (c(lat1 * p) * c(lat2 * p) * (1 - c((lon2 - lon1) * p))) / 2;
+ const a = 0.5 - c((lat2 - lat1) * p) / 2 + (c(lat1 * p) * c(lat2 * p) * (1 - c((lon2 - lon1) * p))) / 2;
return 12742 * Math.asin(Math.sqrt(a)); // 2 * R; R = 6371 km
}
diff --git a/src/gateway/Server.ts b/src/gateway/Server.ts
index 269cc2ed2..2ad3e4136 100644
--- a/src/gateway/Server.ts
+++ b/src/gateway/Server.ts
@@ -28,6 +28,7 @@ import ws from "ws";
import { Connection } from "./events/Connection";
import http from "http";
import { cleanupOnStartup } from "./util/Utils";
+import { randomString } from "@spacebar/api";
export class Server {
public ws: ws.Server;
@@ -50,6 +51,10 @@ export class Server {
if (server) this.server = server;
else {
this.server = http.createServer(function (req, res) {
+ if(!req.headers.cookie?.split("; ").find(x => x.startsWith("__sb_sessid="))) {
+ res.setHeader("Set-Cookie", `__sb_sessid=${randomString(32)}; Secure; HttpOnly; SameSite=None; Path=/`);
+ }
+
res.writeHead(200).end("Online");
});
}
diff --git a/src/gateway/events/Connection.ts b/src/gateway/events/Connection.ts
index bc74dee65..9b132d4b9 100644
--- a/src/gateway/events/Connection.ts
+++ b/src/gateway/events/Connection.ts
@@ -72,6 +72,13 @@ export async function Connection(
);
}
+ if (request.headers.cookie?.split("; ").find(x => x.startsWith("__sb_sessid="))) {
+ socket.fingerprint = request.headers.cookie
+ .split("; ")
+ .find((x) => x.startsWith("__sb_sessid="))
+ ?.split("=")[1];
+ }
+
//Create session ID when the connection is opened. This allows gateway dump to group the initial websocket messages with the rest of the conversation.
const session_id = genSessionId();
socket.session_id = session_id; //Set the session of the WebSocket object
diff --git a/src/gateway/listener/listener.ts b/src/gateway/listener/listener.ts
index d7c3d23e5..f76adfb29 100644
--- a/src/gateway/listener/listener.ts
+++ b/src/gateway/listener/listener.ts
@@ -29,6 +29,7 @@ import {
Message,
NewUrlUserSignatureData,
GuildMemberAddEvent,
+ Ban,
} from "@spacebar/util";
import { OPCODES } from "../util/Constants";
import { Send } from "../util/Send";
@@ -36,7 +37,7 @@ import { WebSocket } from "@spacebar/gateway";
import { Channel as AMQChannel } from "amqplib";
import { Recipient } from "@spacebar/util";
import * as console from "node:console";
-import { PublicMember, RelationshipType } from "@spacebar/schemas"
+import { PublicMember, RelationshipType } from "@spacebar/schemas";
import { bgRedBright } from "picocolors";
// TODO: close connection on Invalidated Token
@@ -46,10 +47,7 @@ import { bgRedBright } from "picocolors";
// Sharding: calculate if the current shard id matches the formula: shard_id = (guild_id >> 22) % num_shards
// https://discord.com/developers/docs/topics/gateway#sharding
-export function handlePresenceUpdate(
- this: WebSocket,
- { event, acknowledge, data }: EventOpts,
-) {
+export function handlePresenceUpdate(this: WebSocket, { event, acknowledge, data }: EventOpts) {
acknowledge?.();
if (event === EVENTEnum.PresenceUpdate) {
return Send(this, {
@@ -92,32 +90,24 @@ export async function setupListener(this: WebSocket) {
this.listen_options = opts;
const consumer = consume.bind(this);
+ const handleChannelError = (err: unknown) => {
+ console.error(`[RabbitMQ] [user-${this.user_id}] Channel Error (Handled):`, err);
+ };
+
console.log("[RabbitMQ] setupListener: open for ", this.user_id);
if (RabbitMQ.connection) {
- console.log(
- "[RabbitMQ] setupListener: opts.channel = ",
- typeof opts.channel,
- "with channel id",
- opts.channel?.ch,
- );
+ console.log("[RabbitMQ] setupListener: opts.channel = ", typeof opts.channel, "with channel id", opts.channel?.ch);
opts.channel = await RabbitMQ.connection.createChannel();
+
+ opts.channel.on("error", handleChannelError);
opts.channel.queues = {};
- console.log(
- "[RabbitMQ] channel created: ",
- typeof opts.channel,
- "with channel id",
- opts.channel?.ch,
- );
+ console.log("[RabbitMQ] channel created: ", typeof opts.channel, "with channel id", opts.channel?.ch);
}
this.events[this.user_id] = await listenEvent(this.user_id, consumer, opts);
relationships.forEach(async (relationship) => {
- this.events[relationship.to_id] = await listenEvent(
- relationship.to_id,
- handlePresenceUpdate.bind(this),
- opts,
- );
+ this.events[relationship.to_id] = await listenEvent(relationship.to_id, handlePresenceUpdate.bind(this), opts);
});
dm_channels.forEach(async (channel) => {
@@ -130,33 +120,27 @@ export async function setupListener(this: WebSocket) {
this.events[guild.id] = await listenEvent(guild.id, consumer, opts);
guild.channels.forEach(async (channel) => {
- if (
- permission
- .overwriteChannel(channel.permission_overwrites ?? [])
- .has("VIEW_CHANNEL")
- ) {
- this.events[channel.id] = await listenEvent(
- channel.id,
- consumer,
- opts,
- );
+ if (permission.overwriteChannel(channel.permission_overwrites ?? []).has("VIEW_CHANNEL")) {
+ this.events[channel.id] = await listenEvent(channel.id, consumer, opts);
}
});
});
- this.once("close", () => {
- console.log(
- "[RabbitMQ] setupListener: close for",
- this.user_id,
- "=",
- typeof opts.channel,
- "with channel id",
- opts.channel?.ch,
+ this.once("close", async () => {
+ console.log("[RabbitMQ] setupListener: close for", this.user_id, "=", typeof opts.channel, "with channel id", opts.channel?.ch);
+
+ // wait for event consumer cancellation
+ await Promise.all(
+ Object.values(this.events).map((x) => {
+ if (x) return x();
+ else return Promise.resolve();
+ }),
);
- if (opts.channel) opts.channel.close();
- else {
- Object.values(this.events).forEach((x) => x?.());
- Object.values(this.member_events).forEach((x) => x());
+ await Promise.all(Object.values(this.member_events).map((x) => x()));
+
+ if (opts.channel) {
+ await opts.channel.close();
+ opts.channel.off("error", handleChannelError);
}
});
}
@@ -180,11 +164,7 @@ async function consume(this: WebSocket, opts: EventOpts) {
break;
case "GUILD_MEMBER_ADD":
if (this.member_events[data.user.id]) break; // already subscribed
- this.member_events[data.user.id] = await listenEvent(
- data.user.id,
- handlePresenceUpdate.bind(this),
- this.listen_options,
- );
+ this.member_events[data.user.id] = await listenEvent(data.user.id, handlePresenceUpdate.bind(this), this.listen_options);
break;
case "GUILD_MEMBER_UPDATE":
if (!this.member_events[data.user.id]) break;
@@ -195,34 +175,32 @@ async function consume(this: WebSocket, opts: EventOpts) {
case "GUILD_DELETE":
this.events[id]?.();
delete this.events[id];
+ if (event === "GUILD_DELETE" && this.ipAddress) {
+ const ban = await Ban.findOne({
+ where: { guild_id: id, user_id: this.user_id },
+ });
+
+ if (ban) {
+ ban.ip = this.ipAddress || undefined;
+ await ban.save();
+ }
+ }
break;
case "CHANNEL_CREATE":
- if (
- !permission
- .overwriteChannel(data.permission_overwrites)
- .has("VIEW_CHANNEL")
- ) {
+ if (!permission.overwriteChannel(data.permission_overwrites).has("VIEW_CHANNEL")) {
return;
}
this.events[id] = await listenEvent(id, consumer, listenOpts);
break;
case "RELATIONSHIP_ADD":
- this.events[data.user.id] = await listenEvent(
- data.user.id,
- handlePresenceUpdate.bind(this),
- this.listen_options,
- );
+ this.events[data.user.id] = await listenEvent(data.user.id, handlePresenceUpdate.bind(this), this.listen_options);
break;
case "GUILD_CREATE":
this.events[id] = await listenEvent(id, consumer, listenOpts);
break;
case "CHANNEL_UPDATE": {
const exists = this.events[id];
- if (
- permission
- .overwriteChannel(data.permission_overwrites)
- .has("VIEW_CHANNEL")
- ) {
+ if (permission.overwriteChannel(data.permission_overwrites).has("VIEW_CHANNEL")) {
if (exists) break;
this.events[id] = await listenEvent(id, consumer, listenOpts);
} else {
@@ -294,20 +272,19 @@ async function consume(this: WebSocket, opts: EventOpts) {
case "MESSAGE_UPDATE":
// console.log(this.request)
if (data["attachments"])
- data["attachments"] =
- Message.prototype.withSignedAttachments.call(
- data,
- new NewUrlUserSignatureData({
- ip: this.ipAddress,
- userAgent: this.userAgent,
- }),
- ).attachments;
+ data["attachments"] = Message.prototype.withSignedAttachments.call(
+ data,
+ new NewUrlUserSignatureData({
+ ip: this.ipAddress,
+ userAgent: this.userAgent,
+ }),
+ ).attachments;
break;
default:
break;
}
- if(event === "GUILD_MEMBER_ADD") {
+ if (event === "GUILD_MEMBER_ADD") {
if ((data as PublicMember).roles === undefined || (data as PublicMember).roles === null) {
console.log(bgRedBright("[Gateway]"), "[GUILD_MEMBER_ADD] roles is undefined, setting to empty array!", opts.origin ?? "(Event origin not defined)", data);
(data as PublicMember).roles = [];
diff --git a/src/gateway/util/WebSocket.ts b/src/gateway/util/WebSocket.ts
index ea6f0701e..d0a42eafa 100644
--- a/src/gateway/util/WebSocket.ts
+++ b/src/gateway/util/WebSocket.ts
@@ -33,6 +33,7 @@ export interface WebSocket extends WS {
compress?: "zlib-stream" | "zstd-stream";
ipAddress?: string;
userAgent?: string; // for cdn request signing
+ fingerprint?: string;
shard_count?: bigint;
shard_id?: bigint;
deflate?: Deflate;
@@ -44,8 +45,8 @@ export interface WebSocket extends WS {
intents: Intents;
sequence: number;
permissions: Record;
- events: Record unknown)>;
- member_events: Record unknown>;
+ events: Record Promise)>;
+ member_events: Record Promise>;
listen_options: ListenEventOpts;
capabilities?: Capabilities;
large_threshold: number;
diff --git a/src/schemas/api/users/InstanceUserDeleteSchema.ts b/src/schemas/api/users/InstanceUserDeleteSchema.ts
new file mode 100644
index 000000000..48c9fde8c
--- /dev/null
+++ b/src/schemas/api/users/InstanceUserDeleteSchema.ts
@@ -0,0 +1,6 @@
+import { ConnectedAccount } from "@spacebar/util";
+
+export type InstanceUserDeleteSchema = InstanceUserDeleteSchemaContent | undefined; //unsure if this a correct way to make the body optional
+export interface InstanceUserDeleteSchemaContent {
+ reason?: string;
+}
diff --git a/src/schemas/api/users/index.ts b/src/schemas/api/users/index.ts
index a95a3282a..5c507121a 100644
--- a/src/schemas/api/users/index.ts
+++ b/src/schemas/api/users/index.ts
@@ -16,6 +16,7 @@
along with this program. If not, see .
*/
export * from "./ConnectedAccount";
+export * from "./InstanceUserDeleteSchema";
export * from "./Member";
export * from "./User";
export * from "./UserSettings";
\ No newline at end of file
diff --git a/src/schemas/uncategorised/BanCreateSchema.ts b/src/schemas/uncategorised/BanCreateSchema.ts
index 6d30ca221..8f349f4bf 100644
--- a/src/schemas/uncategorised/BanCreateSchema.ts
+++ b/src/schemas/uncategorised/BanCreateSchema.ts
@@ -17,7 +17,7 @@
*/
export interface BanCreateSchema {
- delete_message_seconds?: string;
- delete_message_days?: string;
+ delete_message_seconds?: number;
+ delete_message_days?: number;
reason?: string;
}
diff --git a/src/util/config/types/SecurityConfiguration.ts b/src/util/config/types/SecurityConfiguration.ts
index ae1c6cb87..1f3a19181 100644
--- a/src/util/config/types/SecurityConfiguration.ts
+++ b/src/util/config/types/SecurityConfiguration.ts
@@ -24,7 +24,7 @@ export class SecurityConfiguration {
twoFactor: TwoFactorConfiguration = new TwoFactorConfiguration();
autoUpdate: boolean | number = true;
requestSignature: string = crypto.randomBytes(32).toString("base64");
- jwtSecret: string = crypto.randomBytes(256).toString("base64");
+ jwtSecret: string | null = null;
// header to get the real user ip address
// X-Forwarded-For for nginx/reverse proxies
// CF-Connecting-IP for cloudflare
@@ -36,7 +36,7 @@ export class SecurityConfiguration {
// https://docs.abuseipdb.com/#api-daily-rate-limits
abuseipdbBlacklistRatelimit: number = 5;
abuseipdbConfidenceScoreTreshold: number = 50;
- ipdataApiKey: string | null = "eca677b284b3bac29eb72f5e496aa9047f26543605efe99ff2ce35c9"; // isnt even valid anymore it seems?
+ ipdataApiKey: string | null = null;
mfaBackupCodeCount: number = 10;
statsWorldReadable: boolean = true;
defaultRegistrationTokenExpiration: number = 1000 * 60 * 60 * 24 * 7; //1 week
diff --git a/src/util/entities/BackupCodes.ts b/src/util/entities/BackupCodes.ts
index 31c763434..a0c9ba4bd 100644
--- a/src/util/entities/BackupCodes.ts
+++ b/src/util/entities/BackupCodes.ts
@@ -20,6 +20,7 @@ import { Column, Entity, JoinColumn, ManyToOne } from "typeorm";
import { BaseClass } from "./BaseClass";
import { User } from "./User";
import crypto from "crypto";
+import { Config } from "../util";
@Entity({
name: "backup_codes",
@@ -41,7 +42,7 @@ export class BackupCode extends BaseClass {
export function generateMfaBackupCodes(user_id: string) {
const backup_codes: BackupCode[] = [];
- for (let i = 0; i < 10; i++) {
+ for (let i = 0; i < Config.get().security.mfaBackupCodeCount; i++) {
const code = BackupCode.create({
user: { id: user_id },
code: crypto.randomBytes(4).toString("hex"), // 8 characters
diff --git a/src/util/entities/Channel.ts b/src/util/entities/Channel.ts
index 782420447..e21669eae 100644
--- a/src/util/entities/Channel.ts
+++ b/src/util/entities/Channel.ts
@@ -379,13 +379,15 @@ export class Channel extends BaseClass {
// TODO Delete attachments from the CDN for messages in the channel
await Channel.delete({ id: channel.id });
- const guild = await Guild.findOneOrFail({
- where: { id: channel.guild_id },
- select: { channel_ordering: true },
- });
+ if (channel.guild_id) {
+ const guild = await Guild.findOneOrFail({
+ where: { id: channel.guild_id },
+ select: { channel_ordering: true },
+ });
- const updatedOrdering = guild.channel_ordering.filter((id) => id != channel.id);
- await Guild.update({ id: channel.guild_id }, { channel_ordering: updatedOrdering });
+ const updatedOrdering = guild.channel_ordering.filter((id) => id != channel.id);
+ await Guild.update({ id: channel.guild_id }, { channel_ordering: updatedOrdering });
+ }
}
static async calculatePosition(channel_id: string, guild_id: string, guild?: Guild) {
diff --git a/src/util/entities/InstanceBan.ts b/src/util/entities/InstanceBan.ts
new file mode 100644
index 000000000..08100a9fb
--- /dev/null
+++ b/src/util/entities/InstanceBan.ts
@@ -0,0 +1,111 @@
+/*
+ Spacebar: A FOSS re-implementation and extension of the Discord.com backend.
+ Copyright (C) 2023 Spacebar and Spacebar Contributors
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published
+ by the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see .
+*/
+
+import { Column, CreateDateColumn, Entity, FindOptionsWhere, JoinColumn, ManyToOne, OneToOne, RelationId } from "typeorm";
+import { BaseClass } from "./BaseClass";
+import { Team } from "./Team";
+import { User } from "./User";
+import { Guild } from "./Guild";
+
+@Entity({
+ name: "instance_bans",
+})
+export class InstanceBan extends BaseClass {
+ @Column({ type: "bigint" })
+ @CreateDateColumn()
+ created_at: Date = new Date();
+
+ @Column()
+ reason: string;
+
+ @Column({ nullable: true })
+ user_id?: string;
+
+ @Column({ nullable: true })
+ fingerprint?: string;
+
+ @Column({ nullable: true })
+ ip_address?: string;
+
+ // chain of trust type tracking
+
+ @Column({ default: false })
+ is_allowlisted: boolean = false;
+
+ @Column({ default: false })
+ is_from_other_instance_ban: boolean = false;
+
+ @Column({ nullable: true })
+ @RelationId((instance_ban: InstanceBan) => instance_ban.origin_instance_ban)
+ origin_instance_ban_id?: string;
+
+ @JoinColumn({ name: "origin_instance_ban_id" })
+ @OneToOne(() => InstanceBan, { nullable: true, onDelete: "SET NULL" })
+ origin_instance_ban?: InstanceBan;
+
+ static async findInstanceBans(opts: { userId?: string; ipAddress?: string; fingerprint?: string; propagateBan?: boolean }) {
+ const optionalChecks: FindOptionsWhere[] = [{ user_id: opts.userId }];
+ if (opts?.ipAddress) optionalChecks.push({ ip_address: opts.ipAddress });
+ if (opts?.fingerprint) optionalChecks.push({ fingerprint: opts.fingerprint });
+ const instanceBans = await InstanceBan.find({ where: optionalChecks });
+
+ const banReasons = [];
+ for (const ban of instanceBans) {
+ if (ban.is_allowlisted) continue;
+ if (opts?.fingerprint && ban.fingerprint === opts.fingerprint) banReasons.push("fingerprint");
+ if (opts?.ipAddress && ban.ip_address === opts.ipAddress) banReasons.push("ipAddress");
+ if (opts?.userId && ban.user_id === opts?.userId) banReasons.push("userId");
+ }
+
+ const banViralityPromises: Promise[] = [];
+ if (opts.propagateBan && banReasons.length > 0) {
+ if (opts?.ipAddress && !instanceBans.find((b) => b.ip_address === opts.ipAddress))
+ banViralityPromises.push(
+ InstanceBan.create({
+ user_id: opts.userId,
+ ip_address: opts.ipAddress,
+ reason: "Propagated from other instance ban",
+ is_from_other_instance_ban: true,
+ origin_instance_ban: instanceBans[0],
+ }).save(),
+ );
+ if (opts?.fingerprint && !instanceBans.find((b) => b.fingerprint === opts.fingerprint))
+ banViralityPromises.push(
+ InstanceBan.create({
+ user_id: opts.userId,
+ fingerprint: opts.fingerprint,
+ reason: "Propagated from other instance ban",
+ is_from_other_instance_ban: true,
+ origin_instance_ban: instanceBans[0],
+ }).save(),
+ );
+ if (opts?.userId && !instanceBans.find((b) => b.user_id === opts.userId))
+ banViralityPromises.push(
+ InstanceBan.create({
+ user_id: opts.userId,
+ reason: "Propagated from other instance ban",
+ is_from_other_instance_ban: true,
+ origin_instance_ban: instanceBans[0],
+ }).save(),
+ );
+ }
+
+ await Promise.all(banViralityPromises);
+ return banReasons;
+ }
+}
diff --git a/src/util/entities/User.ts b/src/util/entities/User.ts
index afa7c00e4..375aaba4c 100644
--- a/src/util/entities/User.ts
+++ b/src/util/entities/User.ts
@@ -359,4 +359,18 @@ export class User extends BaseClass {
return qry[0];
}
+
+ async getDmChannels() {
+ const qry = await Channel.getRepository()
+ .createQueryBuilder("channel")
+ .leftJoinAndSelect("channel.recipients", "rcp")
+ .where("channel.type = :type", { type: ChannelType.DM })
+ .andWhere("rcp.user_id = :user_id", { user_id: this.id })
+ .groupBy("channel.id")
+ .addGroupBy("rcp.id")
+ .having("COUNT(rcp.id) = 2")
+ .getMany();
+
+ return qry;
+ }
}
diff --git a/src/util/entities/index.ts b/src/util/entities/index.ts
index 11494d00d..0e67675a8 100644
--- a/src/util/entities/index.ts
+++ b/src/util/entities/index.ts
@@ -36,6 +36,7 @@ export * from "./EmbedCache";
export * from "./Emoji";
export * from "./Encryption";
export * from "./Guild";
+export * from "./InstanceBan";
export * from "./Invite";
export * from "./Member";
export * from "./Message";
diff --git a/src/util/interfaces/Event.ts b/src/util/interfaces/Event.ts
index 24157efd1..d0a604937 100644
--- a/src/util/interfaces/Event.ts
+++ b/src/util/interfaces/Event.ts
@@ -219,6 +219,7 @@ export interface GuildBanAddEvent extends Event {
data: {
guild_id: string;
user: User;
+ delete_message_secs?: number
};
}
diff --git a/src/util/migration/postgres-initial.ts b/src/util/migration/postgres-initial.ts
new file mode 100644
index 000000000..769794175
--- /dev/null
+++ b/src/util/migration/postgres-initial.ts
@@ -0,0 +1,727 @@
+// @prettier-ignore
+/*
+ Spacebar: A FOSS re-implementation and extension of the Discord.com backend.
+ Copyright (C) 2025 Spacebar and Spacebar Contributors
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published
+ by the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see .
+*/
+
+import { MigrationInterface, QueryRunner } from "typeorm";
+export class initial0 implements MigrationInterface {
+ name = "initial0";
+ public async up(queryRunner: QueryRunner): Promise {
+ await queryRunner.query(`CREATE TABLE public.applications (
+ id character varying NOT NULL,
+ name character varying NOT NULL,
+ icon character varying,
+ description character varying,
+ summary character varying,
+ type text,
+ hook boolean NOT NULL,
+ bot_public boolean NOT NULL,
+ bot_require_code_grant boolean NOT NULL,
+ verify_key character varying NOT NULL,
+ flags integer NOT NULL,
+ redirect_uris text,
+ rpc_application_state integer,
+ store_application_state integer,
+ verification_state integer,
+ interactions_endpoint_url character varying,
+ integration_public boolean,
+ integration_require_code_grant boolean,
+ discoverability_state integer,
+ discovery_eligibility_flags integer,
+ tags text,
+ cover_image character varying,
+ install_params text,
+ terms_of_service_url character varying,
+ privacy_policy_url character varying,
+ owner_id character varying,
+ bot_user_id character varying,
+ team_id character varying
+ );`);
+
+ await queryRunner.query(`CREATE TABLE public.attachments (
+ id character varying NOT NULL,
+ filename character varying NOT NULL,
+ size integer NOT NULL,
+ url character varying NOT NULL,
+ proxy_url character varying NOT NULL,
+ height integer,
+ width integer,
+ content_type character varying,
+ message_id character varying
+ );`);
+
+ await queryRunner.query(`CREATE TABLE public.audit_logs (
+ id character varying NOT NULL,
+ user_id character varying,
+ action_type integer NOT NULL,
+ options text,
+ changes text NOT NULL,
+ reason character varying,
+ target_id character varying
+ );`);
+
+ await queryRunner.query(`CREATE TABLE public.backup_codes (
+ id character varying NOT NULL,
+ code character varying NOT NULL,
+ consumed boolean NOT NULL,
+ expired boolean NOT NULL,
+ user_id character varying
+ );`);
+
+ await queryRunner.query(`CREATE TABLE public.bans (
+ id character varying NOT NULL,
+ user_id character varying,
+ guild_id character varying,
+ executor_id character varying,
+ ip character varying NOT NULL,
+ reason character varying
+ );`);
+
+ await queryRunner.query(`CREATE TABLE public.categories (
+ id integer NOT NULL,
+ name character varying,
+ localizations text NOT NULL,
+ is_primary boolean
+ );`);
+
+ await queryRunner.query(`CREATE TABLE public.channels (
+ id character varying NOT NULL,
+ created_at timestamp without time zone NOT NULL,
+ name character varying,
+ icon text,
+ type integer NOT NULL,
+ last_message_id character varying,
+ guild_id character varying,
+ parent_id character varying,
+ owner_id character varying,
+ last_pin_timestamp integer,
+ default_auto_archive_duration integer,
+ permission_overwrites text,
+ video_quality_mode integer,
+ bitrate integer,
+ user_limit integer,
+ nsfw boolean NOT NULL,
+ rate_limit_per_user integer,
+ topic character varying,
+ retention_policy_id character varying,
+ flags integer NOT NULL,
+ default_thread_rate_limit_per_user integer NOT NULL,
+ position integer NOT NULL DEFAULT 0
+ );`);
+
+ await queryRunner.query(`CREATE TABLE public.client_release (
+ id character varying NOT NULL,
+ name character varying NOT NULL,
+ pub_date timestamp without time zone NOT NULL,
+ url character varying NOT NULL,
+ platform character varying NOT NULL,
+ enabled boolean NOT NULL,
+ notes character varying
+ );`);
+
+ await queryRunner.query(`CREATE TABLE public.config (
+ key character varying NOT NULL,
+ value text
+ );`);
+
+ await queryRunner.query(`CREATE TABLE public.connected_accounts (
+ id character varying NOT NULL,
+ external_id character varying NOT NULL,
+ user_id character varying,
+ friend_sync boolean NOT NULL,
+ name character varying NOT NULL,
+ revoked boolean NOT NULL,
+ show_activity integer NOT NULL,
+ type character varying NOT NULL,
+ verified boolean NOT NULL,
+ visibility integer NOT NULL,
+ integrations text NOT NULL,
+ metadata text,
+ metadata_visibility integer NOT NULL,
+ two_way_link boolean NOT NULL,
+ token_data text
+ );`);
+
+ await queryRunner.query(`CREATE TABLE public.connection_config (
+ key character varying NOT NULL,
+ value text
+ );`);
+
+ await queryRunner.query(`CREATE TABLE public.embed_cache (
+ id character varying NOT NULL,
+ url character varying NOT NULL,
+ embed text NOT NULL
+ );`);
+
+ await queryRunner.query(`CREATE TABLE public.emojis (
+ id character varying NOT NULL,
+ animated boolean NOT NULL,
+ available boolean NOT NULL,
+ guild_id character varying NOT NULL,
+ user_id character varying,
+ managed boolean NOT NULL,
+ name character varying NOT NULL,
+ require_colons boolean NOT NULL,
+ roles text NOT NULL,
+ groups text
+ );`);
+
+ await queryRunner.query(`CREATE TABLE public.guilds (
+ id character varying NOT NULL,
+ afk_channel_id character varying,
+ afk_timeout integer,
+ banner character varying,
+ default_message_notifications integer,
+ description character varying,
+ discovery_splash character varying,
+ explicit_content_filter integer,
+ features text NOT NULL,
+ primary_category_id character varying,
+ icon character varying,
+ large boolean NOT NULL,
+ max_members integer,
+ max_presences integer,
+ max_video_channel_users integer,
+ member_count integer,
+ presence_count integer,
+ template_id character varying,
+ mfa_level integer,
+ name character varying NOT NULL,
+ owner_id character varying,
+ preferred_locale character varying,
+ premium_subscription_count integer,
+ premium_tier integer NOT NULL,
+ public_updates_channel_id character varying,
+ rules_channel_id character varying,
+ region character varying,
+ splash character varying,
+ system_channel_id character varying,
+ system_channel_flags integer,
+ unavailable boolean NOT NULL,
+ verification_level integer,
+ welcome_screen text NOT NULL,
+ widget_channel_id character varying,
+ widget_enabled boolean NOT NULL,
+ nsfw_level integer,
+ nsfw boolean NOT NULL,
+ parent character varying,
+ premium_progress_bar_enabled boolean
+ );`);
+
+ await queryRunner.query(`CREATE TABLE public.invites (
+ code character varying NOT NULL,
+ temporary boolean NOT NULL,
+ uses integer NOT NULL,
+ max_uses integer NOT NULL,
+ max_age integer NOT NULL,
+ created_at timestamp without time zone NOT NULL,
+ expires_at timestamp without time zone,
+ guild_id character varying,
+ channel_id character varying,
+ inviter_id character varying,
+ target_user_id character varying,
+ target_user_type integer,
+ vanity_url boolean,
+ flags integer NOT NULL
+ );`);
+
+ await queryRunner.query(`CREATE TABLE public.member_roles (
+ index integer NOT NULL,
+ role_id character varying NOT NULL
+ );`);
+
+ await queryRunner.query(`CREATE TABLE public.members (
+ index integer NOT NULL,
+ id character varying NOT NULL,
+ guild_id character varying NOT NULL,
+ nick character varying,
+ joined_at timestamp without time zone NOT NULL,
+ premium_since bigint,
+ deaf boolean NOT NULL,
+ mute boolean NOT NULL,
+ pending boolean NOT NULL,
+ settings text NOT NULL,
+ last_message_id character varying,
+ joined_by character varying,
+ avatar character varying,
+ banner character varying,
+ bio character varying NOT NULL,
+ theme_colors text,
+ pronouns character varying,
+ communication_disabled_until timestamp without time zone
+ );`);
+
+ await queryRunner.query(`CREATE SEQUENCE public.members_index_seq
+ AS integer
+ START WITH 1
+ INCREMENT BY 1
+ NO MINVALUE
+ NO MAXVALUE
+ CACHE 1;`);
+
+ await queryRunner.query(`ALTER SEQUENCE public.members_index_seq OWNED BY public.members.index;`);
+ await queryRunner.query(`CREATE TABLE public.message_channel_mentions (
+ "messagesId" character varying NOT NULL,
+ "channelsId" character varying NOT NULL
+ );`);
+
+ await queryRunner.query(`CREATE TABLE public.message_role_mentions (
+ "messagesId" character varying NOT NULL,
+ "rolesId" character varying NOT NULL
+ );`);
+
+ await queryRunner.query(`CREATE TABLE public.message_stickers (
+ "messagesId" character varying NOT NULL,
+ "stickersId" character varying NOT NULL
+ );`);
+
+ await queryRunner.query(`CREATE TABLE public.message_user_mentions (
+ "messagesId" character varying NOT NULL,
+ "usersId" character varying NOT NULL
+ );`);
+
+ await queryRunner.query(`CREATE TABLE public.messages (
+ id character varying NOT NULL,
+ channel_id character varying,
+ guild_id character varying,
+ author_id character varying,
+ member_id character varying,
+ webhook_id character varying,
+ application_id character varying,
+ content character varying,
+ "timestamp" timestamp without time zone DEFAULT now() NOT NULL,
+ edited_timestamp timestamp without time zone,
+ tts boolean,
+ mention_everyone boolean,
+ embeds text NOT NULL,
+ reactions text NOT NULL,
+ nonce text,
+ type integer NOT NULL,
+ activity text,
+ message_reference text,
+ interaction text,
+ components text,
+ message_reference_id character varying,
+ pinned boolean,
+ flags integer
+ );`);
+
+ await queryRunner.query(`CREATE TABLE public.migrations (
+ id integer NOT NULL,
+ "timestamp" bigint NOT NULL,
+ name character varying NOT NULL
+ );`);
+
+ await queryRunner.query(`CREATE SEQUENCE public.migrations_id_seq
+ AS integer
+ START WITH 1
+ INCREMENT BY 1
+ NO MINVALUE
+ NO MAXVALUE
+ CACHE 1;`);
+
+ await queryRunner.query(`ALTER SEQUENCE public.migrations_id_seq OWNED BY public.migrations.id;`);
+ await queryRunner.query(`CREATE TABLE public.notes (
+ id character varying NOT NULL,
+ content character varying NOT NULL,
+ owner_id character varying,
+ target_id character varying
+ );`);
+
+ await queryRunner.query(`CREATE TABLE public.rate_limits (
+ id character varying NOT NULL,
+ executor_id character varying NOT NULL,
+ hits integer NOT NULL,
+ blocked boolean NOT NULL,
+ expires_at timestamp without time zone NOT NULL
+ );`);
+
+ await queryRunner.query(`CREATE TABLE public.read_states (
+ id character varying NOT NULL,
+ channel_id character varying NOT NULL,
+ user_id character varying NOT NULL,
+ last_message_id character varying,
+ public_ack character varying,
+ notifications_cursor character varying,
+ last_pin_timestamp timestamp without time zone,
+ mention_count integer
+ );`);
+
+ await queryRunner.query(`CREATE TABLE public.recipients (
+ id character varying NOT NULL,
+ channel_id character varying NOT NULL,
+ user_id character varying NOT NULL,
+ closed boolean DEFAULT false NOT NULL
+ );`);
+
+ await queryRunner.query(`CREATE TABLE public.relationships (
+ id character varying NOT NULL,
+ from_id character varying NOT NULL,
+ to_id character varying NOT NULL,
+ nickname character varying,
+ type integer NOT NULL
+ );`);
+
+ await queryRunner.query(`CREATE TABLE public.roles (
+ id character varying NOT NULL,
+ guild_id character varying NOT NULL,
+ color integer NOT NULL,
+ hoist boolean NOT NULL,
+ managed boolean NOT NULL,
+ mentionable boolean NOT NULL,
+ name character varying NOT NULL,
+ permissions character varying NOT NULL,
+ "position" integer NOT NULL,
+ icon character varying,
+ unicode_emoji character varying,
+ tags text,
+ flags integer DEFAULT 0 NOT NULL
+ );`);
+
+ await queryRunner.query(`CREATE TABLE public.security_settings (
+ id character varying NOT NULL,
+ guild_id character varying,
+ channel_id character varying,
+ encryption_permission_mask integer NOT NULL,
+ allowed_algorithms text NOT NULL,
+ current_algorithm character varying NOT NULL,
+ used_since_message character varying
+ );`);
+
+ await queryRunner.query(`CREATE TABLE public.sessions (
+ id character varying NOT NULL,
+ user_id character varying,
+ session_id character varying NOT NULL,
+ activities text,
+ client_info text NOT NULL,
+ status character varying NOT NULL
+ );`);
+
+ await queryRunner.query(`CREATE TABLE public.sticker_packs (
+ id character varying NOT NULL,
+ name character varying NOT NULL,
+ description character varying,
+ banner_asset_id character varying,
+ cover_sticker_id character varying,
+ "coverStickerId" character varying
+ );`);
+
+ await queryRunner.query(`CREATE TABLE public.stickers (
+ id character varying NOT NULL,
+ name character varying NOT NULL,
+ description character varying,
+ available boolean,
+ tags character varying,
+ pack_id character varying,
+ guild_id character varying,
+ user_id character varying,
+ type integer NOT NULL,
+ format_type integer NOT NULL
+ );`);
+
+ await queryRunner.query(`CREATE TABLE public.team_members (
+ id character varying NOT NULL,
+ membership_state integer NOT NULL,
+ permissions text NOT NULL,
+ team_id character varying,
+ user_id character varying
+ );`);
+
+ await queryRunner.query(`CREATE TABLE public.teams (
+ id character varying NOT NULL,
+ icon character varying,
+ name character varying NOT NULL,
+ owner_user_id character varying
+ );`);
+
+ await queryRunner.query(`CREATE TABLE public.templates (
+ id character varying NOT NULL,
+ code character varying NOT NULL,
+ name character varying NOT NULL,
+ description character varying,
+ usage_count integer,
+ creator_id character varying,
+ created_at timestamp without time zone NOT NULL,
+ updated_at timestamp without time zone NOT NULL,
+ source_guild_id character varying,
+ serialized_source_guild text NOT NULL
+ );`);
+
+ await queryRunner.query(`CREATE TABLE public.user_settings (
+ index integer NOT NULL,
+ afk_timeout integer,
+ allow_accessibility_detection boolean,
+ animate_emoji boolean,
+ animate_stickers integer,
+ contact_sync_enabled boolean,
+ convert_emoticons boolean,
+ custom_status text,
+ default_guilds_restricted boolean,
+ detect_platform_accounts boolean,
+ developer_mode boolean,
+ disable_games_tab boolean,
+ enable_tts_command boolean,
+ explicit_content_filter integer,
+ friend_source_flags text,
+ gateway_connected boolean,
+ gif_auto_play boolean,
+ guild_folders text,
+ guild_positions text,
+ inline_attachment_media boolean,
+ inline_embed_media boolean,
+ locale character varying,
+ message_display_compact boolean,
+ native_phone_integration_enabled boolean,
+ render_embeds boolean,
+ render_reactions boolean,
+ restricted_guilds text,
+ show_current_game boolean,
+ status character varying,
+ stream_notifications_enabled boolean,
+ theme character varying,
+ timezone_offset integer
+ );`);
+
+ await queryRunner.query(`CREATE SEQUENCE public.user_settings_index_seq
+ AS integer
+ START WITH 1
+ INCREMENT BY 1
+ NO MINVALUE
+ NO MAXVALUE
+ CACHE 1;`
+ );
+
+ await queryRunner.query(`ALTER SEQUENCE public.user_settings_index_seq OWNED BY public.user_settings.index;`);
+ await queryRunner.query(`CREATE TABLE public.users (
+ id character varying NOT NULL,
+ username character varying NOT NULL,
+ discriminator character varying NOT NULL,
+ avatar character varying,
+ accent_color integer,
+ banner character varying,
+ theme_colors text,
+ pronouns character varying,
+ phone character varying,
+ desktop boolean NOT NULL,
+ mobile boolean NOT NULL,
+ premium boolean NOT NULL,
+ premium_type integer NOT NULL,
+ bot boolean NOT NULL,
+ bio character varying NOT NULL,
+ system boolean NOT NULL,
+ nsfw_allowed boolean NOT NULL,
+ mfa_enabled boolean NOT NULL,
+ totp_secret character varying,
+ totp_last_ticket character varying,
+ created_at timestamp without time zone NOT NULL,
+ premium_since timestamp without time zone,
+ verified boolean NOT NULL,
+ disabled boolean NOT NULL,
+ deleted boolean NOT NULL,
+ email character varying,
+ flags bigint NOT NULL,
+ public_flags bigint NOT NULL,
+ purchased_flags bigint NOT NULL,
+ premium_usage_flags integer NOT NULL,
+ rights bigint NOT NULL,
+ data text NOT NULL,
+ fingerprints text NOT NULL,
+ extended_settings text NOT NULL,
+ "settingsIndex" integer
+ );`);
+
+ await queryRunner.query(`CREATE TABLE public.valid_registration_tokens (
+ token character varying NOT NULL,
+ created_at timestamp without time zone NOT NULL,
+ expires_at timestamp without time zone NOT NULL
+ );`);
+
+ await queryRunner.query(`CREATE TABLE public.voice_states (
+ id character varying NOT NULL,
+ guild_id character varying,
+ channel_id character varying,
+ user_id character varying,
+ session_id character varying NOT NULL,
+ token character varying,
+ deaf boolean NOT NULL,
+ mute boolean NOT NULL,
+ self_deaf boolean NOT NULL,
+ self_mute boolean NOT NULL,
+ self_stream boolean,
+ self_video boolean NOT NULL,
+ suppress boolean NOT NULL,
+ request_to_speak_timestamp timestamp without time zone
+ );`);
+
+ await queryRunner.query(`CREATE TABLE public.webhooks (
+ id character varying NOT NULL,
+ type integer NOT NULL,
+ name character varying,
+ avatar character varying,
+ token character varying,
+ guild_id character varying,
+ channel_id character varying,
+ application_id character varying,
+ user_id character varying,
+ source_guild_id character varying
+ );`);
+
+ await queryRunner.query(`ALTER TABLE ONLY public.members ALTER COLUMN index SET DEFAULT nextval('public.members_index_seq'::regclass);`);
+ await queryRunner.query(`ALTER TABLE ONLY public.migrations ALTER COLUMN id SET DEFAULT nextval('public.migrations_id_seq'::regclass);`);
+ await queryRunner.query(`ALTER TABLE ONLY public.user_settings ALTER COLUMN index SET DEFAULT nextval('public.user_settings_index_seq'::regclass);`);
+ await queryRunner.query(`ALTER TABLE ONLY public.embed_cache ADD CONSTRAINT "PK_0abb7581d4efc5a8b1361389c5e" PRIMARY KEY (id);`);
+ await queryRunner.query(`ALTER TABLE ONLY public.messages ADD CONSTRAINT "PK_18325f38ae6de43878487eff986" PRIMARY KEY (id);`);
+ await queryRunner.query(`ALTER TABLE ONLY public.audit_logs ADD CONSTRAINT "PK_1bb179d048bbc581caa3b013439" PRIMARY KEY (id);`);
+ await queryRunner.query(`ALTER TABLE ONLY public.categories ADD CONSTRAINT "PK_24dbc6126a28ff948da33e97d3b" PRIMARY KEY (id);`);
+ await queryRunner.query(`ALTER TABLE ONLY public.config ADD CONSTRAINT "PK_26489c99ddbb4c91631ef5cc791" PRIMARY KEY (key);`);
+ await queryRunner.query(`ALTER TABLE ONLY public.sessions ADD CONSTRAINT "PK_3238ef96f18b355b671619111bc" PRIMARY KEY (id);`);
+ await queryRunner.query(`ALTER TABLE ONLY public.invites ADD CONSTRAINT "PK_33fd8a248db1cd832baa8aa25bf" PRIMARY KEY (code);`);
+ await queryRunner.query(`ALTER TABLE ONLY public.backup_codes ADD CONSTRAINT "PK_34ab957382dbc57e8fb53f1638f" PRIMARY KEY (id);`);
+ await queryRunner.query(`ALTER TABLE ONLY public.rate_limits ADD CONSTRAINT "PK_3b4449f1f5fc167d921ee619f65" PRIMARY KEY (id);`);
+ await queryRunner.query(`ALTER TABLE ONLY public.security_settings ADD CONSTRAINT "PK_4aec436cf81177ae97a1bcec3c7" PRIMARY KEY (id);`);
+ await queryRunner.query(`ALTER TABLE ONLY public.client_release ADD CONSTRAINT "PK_4c4ea258342d2d6ba1be0a71a43" PRIMARY KEY (id);`);
+ await queryRunner.query(`ALTER TABLE ONLY public.templates ADD CONSTRAINT "PK_515948649ce0bbbe391de702ae5" PRIMARY KEY (id);`);
+ await queryRunner.query(`ALTER TABLE ONLY public.attachments ADD CONSTRAINT "PK_5e1f050bcff31e3084a1d662412" PRIMARY KEY (id);`);
+ await queryRunner.query(`ALTER TABLE ONLY public.connected_accounts ADD CONSTRAINT "PK_70416f1da0be645bb31da01c774" PRIMARY KEY (id);`);
+ await queryRunner.query(`ALTER TABLE ONLY public.message_role_mentions ADD CONSTRAINT "PK_74dba92cc300452a6e14b83ed44" PRIMARY KEY ("messagesId", "rolesId");`);
+ await queryRunner.query(`ALTER TABLE ONLY public.teams ADD CONSTRAINT "PK_7e5523774a38b08a6236d322403" PRIMARY KEY (id);`);
+ await queryRunner.query(`ALTER TABLE ONLY public.message_channel_mentions ADD CONSTRAINT "PK_85cb45351497cd9d06a79ced65e" PRIMARY KEY ("messagesId", "channelsId");`);
+ await queryRunner.query(`ALTER TABLE ONLY public.migrations ADD CONSTRAINT "PK_8c82d7f526340ab734260ea46be" PRIMARY KEY (id);`);
+ await queryRunner.query(`ALTER TABLE ONLY public.applications ADD CONSTRAINT "PK_938c0a27255637bde919591888f" PRIMARY KEY (id);`);
+ await queryRunner.query(`ALTER TABLE ONLY public.member_roles ADD CONSTRAINT "PK_951c1d72a0fd1da8760b4a1fd66" PRIMARY KEY (index, role_id);`);
+ await queryRunner.query(`ALTER TABLE ONLY public.emojis ADD CONSTRAINT "PK_9adb96a675f555c6169bad7ba62" PRIMARY KEY (id);`);
+ await queryRunner.query(`ALTER TABLE ONLY public.message_user_mentions ADD CONSTRAINT "PK_9b9b6e245ad47a48dbd7605d4fb" PRIMARY KEY ("messagesId", "usersId");`);
+ await queryRunner.query(`ALTER TABLE ONLY public.webhooks ADD CONSTRAINT "PK_9e8795cfc899ab7bdaa831e8527" PRIMARY KEY (id);`);
+ await queryRunner.query(`ALTER TABLE ONLY public.sticker_packs ADD CONSTRAINT "PK_a27381efea0f876f5d3233af655" PRIMARY KEY (id);`);
+ await queryRunner.query(`ALTER TABLE ONLY public.users ADD CONSTRAINT "PK_a3ffb1c0c8416b9fc6f907b7433" PRIMARY KEY (id);`);
+ await queryRunner.query(`ALTER TABLE ONLY public.bans ADD CONSTRAINT "PK_a4d6f261bffa4615c62d756566a" PRIMARY KEY (id);`);
+ await queryRunner.query(`ALTER TABLE ONLY public.voice_states ADD CONSTRAINT "PK_ada09a50c134fad1369b510e3ce" PRIMARY KEY (id);`);
+ await queryRunner.query(`ALTER TABLE ONLY public.notes ADD CONSTRAINT "PK_af6206538ea96c4e77e9f400c3d" PRIMARY KEY (id);`);
+ await queryRunner.query(`ALTER TABLE ONLY public.members ADD CONSTRAINT "PK_b4a6b8c2478e5df990909c6cf6a" PRIMARY KEY (index);`);
+ await queryRunner.query(`ALTER TABLE ONLY public.relationships ADD CONSTRAINT "PK_ba20e2f5cf487408e08e4dcecaf" PRIMARY KEY (id);`);
+ await queryRunner.query(`ALTER TABLE ONLY public.connection_config ADD CONSTRAINT "PK_bc0554f736ad71dde346549488a" PRIMARY KEY (key);`);
+ await queryRunner.query(`ALTER TABLE ONLY public.channels ADD CONSTRAINT "PK_bc603823f3f741359c2339389f9" PRIMARY KEY (id);`);
+ await queryRunner.query(`ALTER TABLE ONLY public.roles ADD CONSTRAINT "PK_c1433d71a4838793a49dcad46ab" PRIMARY KEY (id);`);
+ await queryRunner.query(`ALTER TABLE ONLY public.team_members ADD CONSTRAINT "PK_ca3eae89dcf20c9fd95bf7460aa" PRIMARY KEY (id);`);
+ await queryRunner.query(`ALTER TABLE ONLY public.recipients ADD CONSTRAINT "PK_de8fc5a9c364568f294798fe1e9" PRIMARY KEY (id);`);
+ await queryRunner.query(`ALTER TABLE ONLY public.valid_registration_tokens ADD CONSTRAINT "PK_e0f5c8e3fcefe3134a092c50485" PRIMARY KEY (token);`);
+ await queryRunner.query(`ALTER TABLE ONLY public.stickers ADD CONSTRAINT "PK_e1dafa4063a5532645cc2810374" PRIMARY KEY (id);`);
+ await queryRunner.query(`ALTER TABLE ONLY public.read_states ADD CONSTRAINT "PK_e6956a804978f01b713b1ed58e2" PRIMARY KEY (id);`);
+ await queryRunner.query(`ALTER TABLE ONLY public.guilds ADD CONSTRAINT "PK_e7e7f2a51bd6d96a9ac2aa560f9" PRIMARY KEY (id);`);
+ await queryRunner.query(`ALTER TABLE ONLY public.user_settings ADD CONSTRAINT "PK_e81f8bb92802737337d35c00981" PRIMARY KEY (index);`);
+ await queryRunner.query(`ALTER TABLE ONLY public.message_stickers ADD CONSTRAINT "PK_ed820c4093d0b8cd1d2bcf66087" PRIMARY KEY ("messagesId", "stickersId");`);
+ await queryRunner.query(`ALTER TABLE ONLY public.users ADD CONSTRAINT "REL_0c14beb78d8c5ccba66072adbc" UNIQUE ("settingsIndex");`);
+ await queryRunner.query(`ALTER TABLE ONLY public.applications ADD CONSTRAINT "REL_2ce5a55796fe4c2f77ece57a64" UNIQUE (bot_user_id);`);
+ await queryRunner.query(`ALTER TABLE ONLY public.notes ADD CONSTRAINT "UQ_74e6689b9568cc965b8bfc9150b" UNIQUE (owner_id, target_id);`);
+ await queryRunner.query(`ALTER TABLE ONLY public.templates ADD CONSTRAINT "UQ_be38737bf339baf63b1daeffb55" UNIQUE (code);`);
+ await queryRunner.query(`CREATE INDEX "IDX_05535bc695e9f7ee104616459d" ON public.messages USING btree (author_id);`);
+ await queryRunner.query(`CREATE UNIQUE INDEX "IDX_0abf8b443321bd3cf7f81ee17a" ON public.read_states USING btree (channel_id, user_id);`);
+ await queryRunner.query(`CREATE INDEX "IDX_29d63eb1a458200851bc37d074" ON public.message_role_mentions USING btree ("rolesId");`);
+ await queryRunner.query(`CREATE INDEX "IDX_2a27102ecd1d81b4582a436092" ON public.message_channel_mentions USING btree ("messagesId");`);
+ await queryRunner.query(`CREATE UNIQUE INDEX "IDX_3ed7a60fb7dbe04e1ba9332a8b" ON public.messages USING btree (channel_id, id);`);
+ await queryRunner.query(`CREATE INDEX "IDX_40bb6f23e7cc133292e92829d2" ON public.message_stickers USING btree ("messagesId");`);
+ await queryRunner.query(`CREATE INDEX "IDX_5d7ddc8a5f9c167f548625e772" ON public.member_roles USING btree (index);`);
+ await queryRunner.query(`CREATE INDEX "IDX_86b9109b155eb70c0a2ca3b4b6" ON public.messages USING btree (channel_id);`);
+ await queryRunner.query(`CREATE UNIQUE INDEX "IDX_a0b2ff0a598df0b0d055934a17" ON public.relationships USING btree (from_id, to_id);`);
+ await queryRunner.query(`CREATE INDEX "IDX_a343387fc560ef378760681c23" ON public.message_user_mentions USING btree ("messagesId");`);
+ await queryRunner.query(`CREATE INDEX "IDX_a8242cf535337a490b0feaea0b" ON public.message_role_mentions USING btree ("messagesId");`);
+ await queryRunner.query(`CREATE INDEX "IDX_b831eb18ceebd28976239b1e2f" ON public.message_user_mentions USING btree ("usersId");`);
+ await queryRunner.query(`CREATE UNIQUE INDEX "IDX_bb2bf9386ac443afbbbf9f12d3" ON public.members USING btree (id, guild_id);`);
+ await queryRunner.query(`CREATE INDEX "IDX_bdb8c09e1464cabf62105bf4b9" ON public.message_channel_mentions USING btree ("channelsId");`);
+ await queryRunner.query(`CREATE INDEX "IDX_e22a70819d07659c7a71c112a1" ON public.message_stickers USING btree ("stickersId");`);
+ await queryRunner.query(`CREATE INDEX "IDX_e9080e7a7997a0170026d5139c" ON public.member_roles USING btree (role_id);`);
+ await queryRunner.query(`ALTER TABLE ONLY public.voice_states ADD CONSTRAINT "FK_03779ef216d4b0358470d9cb748" FOREIGN KEY (guild_id) REFERENCES public.guilds(id) ON DELETE CASCADE;`);
+ await queryRunner.query(`ALTER TABLE ONLY public.messages ADD CONSTRAINT "FK_05535bc695e9f7ee104616459d3" FOREIGN KEY (author_id) REFERENCES public.users(id) ON DELETE CASCADE;`);
+ await queryRunner.query(`ALTER TABLE ONLY public.bans ADD CONSTRAINT "FK_07ad88c86d1f290d46748410d58" FOREIGN KEY (executor_id) REFERENCES public.users(id);`);
+ await queryRunner.query(`ALTER TABLE ONLY public.sessions ADD CONSTRAINT "FK_085d540d9f418cfbdc7bd55bb19" FOREIGN KEY (user_id) REFERENCES public.users(id) ON DELETE CASCADE;`);
+ await queryRunner.query(`ALTER TABLE ONLY public.users ADD CONSTRAINT "FK_0c14beb78d8c5ccba66072adbc7" FOREIGN KEY ("settingsIndex") REFERENCES public.user_settings(index);`);
+ await queryRunner.query(`ALTER TABLE ONLY public.webhooks ADD CONSTRAINT "FK_0d523f6f997c86e052c49b1455f" FOREIGN KEY (user_id) REFERENCES public.users(id) ON DELETE CASCADE;`);
+ await queryRunner.query(`ALTER TABLE ONLY public.invites ADD CONSTRAINT "FK_11a0d394f8fc649c19ce5f16b59" FOREIGN KEY (target_user_id) REFERENCES public.users(id) ON DELETE CASCADE;`);
+ await queryRunner.query(`ALTER TABLE ONLY public.teams ADD CONSTRAINT "FK_13f00abf7cb6096c43ecaf8c108" FOREIGN KEY (owner_user_id) REFERENCES public.users(id);`);
+ await queryRunner.query(`ALTER TABLE ONLY public.invites ADD CONSTRAINT "FK_15c35422032e0b22b4ada95f48f" FOREIGN KEY (inviter_id) REFERENCES public.users(id) ON DELETE CASCADE;`);
+ await queryRunner.query(`ALTER TABLE ONLY public.members ADD CONSTRAINT "FK_16aceddd5b89825b8ed6029ad1c" FOREIGN KEY (guild_id) REFERENCES public.guilds(id) ON DELETE CASCADE;`);
+ await queryRunner.query(`ALTER TABLE ONLY public.stickers ADD CONSTRAINT "FK_193d551d852aca5347ef5c9f205" FOREIGN KEY (guild_id) REFERENCES public.guilds(id) ON DELETE CASCADE;`);
+ await queryRunner.query(`ALTER TABLE ONLY public.read_states ADD CONSTRAINT "FK_195f92e4dd1254a4e348c043763" FOREIGN KEY (user_id) REFERENCES public.users(id) ON DELETE CASCADE;`);
+ await queryRunner.query(`ALTER TABLE ONLY public.notes ADD CONSTRAINT "FK_23e08e5b4481711d573e1abecdc" FOREIGN KEY (target_id) REFERENCES public.users(id) ON DELETE CASCADE;`);
+ await queryRunner.query(`ALTER TABLE ONLY public.members ADD CONSTRAINT "FK_28b53062261b996d9c99fa12404" FOREIGN KEY (id) REFERENCES public.users(id) ON DELETE CASCADE;`);
+ await queryRunner.query(`ALTER TABLE ONLY public.message_role_mentions ADD CONSTRAINT "FK_29d63eb1a458200851bc37d074b" FOREIGN KEY ("rolesId") REFERENCES public.roles(id) ON UPDATE CASCADE ON DELETE CASCADE;`);
+ await queryRunner.query(`ALTER TABLE ONLY public.message_channel_mentions ADD CONSTRAINT "FK_2a27102ecd1d81b4582a4360921" FOREIGN KEY ("messagesId") REFERENCES public.messages(id) ON UPDATE CASCADE ON DELETE CASCADE;`);
+ await queryRunner.query(`ALTER TABLE ONLY public.applications ADD CONSTRAINT "FK_2ce5a55796fe4c2f77ece57a647" FOREIGN KEY (bot_user_id) REFERENCES public.users(id) ON DELETE CASCADE;`);
+ await queryRunner.query(`ALTER TABLE ONLY public.recipients ADD CONSTRAINT "FK_2f18ee1ba667f233ae86c0ea60e" FOREIGN KEY (channel_id) REFERENCES public.channels(id) ON DELETE CASCADE;`);
+ await queryRunner.query(`ALTER TABLE ONLY public.channels ADD CONSTRAINT "FK_3274522d14af40540b1a883fc80" FOREIGN KEY (parent_id) REFERENCES public.channels(id);`);
+ await queryRunner.query(`ALTER TABLE ONLY public.channels ADD CONSTRAINT "FK_3873ed438575cce703ecff4fc7b" FOREIGN KEY (owner_id) REFERENCES public.users(id);`);
+ await queryRunner.query(`ALTER TABLE ONLY public.webhooks ADD CONSTRAINT "FK_3a285f4f49c40e0706d3018bc9f" FOREIGN KEY (source_guild_id) REFERENCES public.guilds(id) ON DELETE CASCADE;`);
+ await queryRunner.query(`ALTER TABLE ONLY public.audit_logs ADD CONSTRAINT "FK_3cd01cd3ae7aab010310d96ac8e" FOREIGN KEY (target_id) REFERENCES public.users(id);`);
+ await queryRunner.query(`ALTER TABLE ONLY public.invites ADD CONSTRAINT "FK_3f4939aa1461e8af57fea3fb05d" FOREIGN KEY (guild_id) REFERENCES public.guilds(id) ON DELETE CASCADE;`);
+ await queryRunner.query(`ALTER TABLE ONLY public.message_stickers ADD CONSTRAINT "FK_40bb6f23e7cc133292e92829d28" FOREIGN KEY ("messagesId") REFERENCES public.messages(id) ON UPDATE CASCADE ON DELETE CASCADE;`);
+ await queryRunner.query(`ALTER TABLE ONLY public.read_states ADD CONSTRAINT "FK_40da2fca4e0eaf7a23b5bfc5d34" FOREIGN KEY (channel_id) REFERENCES public.channels(id) ON DELETE CASCADE;`);
+ await queryRunner.query(`ALTER TABLE ONLY public.templates ADD CONSTRAINT "FK_445d00eaaea0e60a017a5ed0c11" FOREIGN KEY (source_guild_id) REFERENCES public.guilds(id);`);
+ await queryRunner.query(`ALTER TABLE ONLY public.sticker_packs ADD CONSTRAINT "FK_448fafba4355ee1c837bbc865f1" FOREIGN KEY ("coverStickerId") REFERENCES public.stickers(id);`);
+ await queryRunner.query(`ALTER TABLE ONLY public.webhooks ADD CONSTRAINT "FK_487a7af59d189f744fe394368fc" FOREIGN KEY (guild_id) REFERENCES public.guilds(id) ON DELETE CASCADE;`);
+ await queryRunner.query(`ALTER TABLE ONLY public.emojis ADD CONSTRAINT "FK_4b988e0db89d94cebcf07f598cc" FOREIGN KEY (guild_id) REFERENCES public.guilds(id) ON DELETE CASCADE;`);
+ await queryRunner.query(`ALTER TABLE ONLY public.bans ADD CONSTRAINT "FK_5999e8e449f80a236ff72023559" FOREIGN KEY (user_id) REFERENCES public.users(id) ON DELETE CASCADE;`);
+ await queryRunner.query(`ALTER TABLE ONLY public.messages ADD CONSTRAINT "FK_5d3ec1cb962de6488637fd779d6" FOREIGN KEY (application_id) REFERENCES public.applications(id);`);
+ await queryRunner.query(`ALTER TABLE ONLY public.member_roles ADD CONSTRAINT "FK_5d7ddc8a5f9c167f548625e772e" FOREIGN KEY (index) REFERENCES public.members(index) ON UPDATE CASCADE ON DELETE CASCADE;`);
+ await queryRunner.query(`ALTER TABLE ONLY public.voice_states ADD CONSTRAINT "FK_5fe1d5f931a67e85039c640001b" FOREIGN KEY (user_id) REFERENCES public.users(id) ON DELETE CASCADE;`);
+ await queryRunner.query(`ALTER TABLE ONLY public.recipients ADD CONSTRAINT "FK_6157e8b6ba4e6e3089616481fe2" FOREIGN KEY (user_id) REFERENCES public.users(id) ON DELETE CASCADE;`);
+ await queryRunner.query(`ALTER TABLE ONLY public.messages ADD CONSTRAINT "FK_61a92bb65b302a76d9c1fcd3174" FOREIGN KEY (message_reference_id) REFERENCES public.messages(id);`);
+ await queryRunner.query(`ALTER TABLE ONLY public.attachments ADD CONSTRAINT "FK_623e10eec51ada466c5038979e3" FOREIGN KEY (message_id) REFERENCES public.messages(id) ON DELETE CASCADE;`);
+ await queryRunner.query(`ALTER TABLE ONLY public.invites ADD CONSTRAINT "FK_6a15b051fe5050aa00a4b9ff0f6" FOREIGN KEY (channel_id) REFERENCES public.channels(id) ON DELETE CASCADE;`);
+ await queryRunner.query(`ALTER TABLE ONLY public.backup_codes ADD CONSTRAINT "FK_70066ea80d2f4b871beda32633b" FOREIGN KEY (user_id) REFERENCES public.users(id) ON DELETE CASCADE;`);
+ await queryRunner.query(`ALTER TABLE ONLY public.messages ADD CONSTRAINT "FK_86b9109b155eb70c0a2ca3b4b6d" FOREIGN KEY (channel_id) REFERENCES public.channels(id) ON DELETE CASCADE;`);
+ await queryRunner.query(`ALTER TABLE ONLY public.guilds ADD CONSTRAINT "FK_8d450b016dc8bec35f36729e4b0" FOREIGN KEY (public_updates_channel_id) REFERENCES public.channels(id);`);
+ await queryRunner.query(`ALTER TABLE ONLY public.stickers ADD CONSTRAINT "FK_8f4ee73f2bb2325ff980502e158" FOREIGN KEY (user_id) REFERENCES public.users(id) ON DELETE CASCADE;`);
+ await queryRunner.query(`ALTER TABLE ONLY public.guilds ADD CONSTRAINT "FK_95828668aa333460582e0ca6396" FOREIGN KEY (rules_channel_id) REFERENCES public.channels(id);`);
+ await queryRunner.query(`ALTER TABLE ONLY public.relationships ADD CONSTRAINT "FK_9af4194bab1250b1c584ae4f1d7" FOREIGN KEY (from_id) REFERENCES public.users(id) ON DELETE CASCADE;`);
+ await queryRunner.query(`ALTER TABLE ONLY public.relationships ADD CONSTRAINT "FK_9c7f6b98a9843b76dce1b0c878b" FOREIGN KEY (to_id) REFERENCES public.users(id) ON DELETE CASCADE;`);
+ await queryRunner.query(`ALTER TABLE ONLY public.guilds ADD CONSTRAINT "FK_9d1d665379eefde7876a17afa99" FOREIGN KEY (widget_channel_id) REFERENCES public.channels(id);`);
+ await queryRunner.query(`ALTER TABLE ONLY public.bans ADD CONSTRAINT "FK_9d3ab7dd180ebdd245cdb66ecad" FOREIGN KEY (guild_id) REFERENCES public.guilds(id) ON DELETE CASCADE;`);
+ await queryRunner.query(`ALTER TABLE ONLY public.voice_states ADD CONSTRAINT "FK_9f8d389866b40b6657edd026dd4" FOREIGN KEY (channel_id) REFERENCES public.channels(id) ON DELETE CASCADE;`);
+ await queryRunner.query(`ALTER TABLE ONLY public.message_user_mentions ADD CONSTRAINT "FK_a343387fc560ef378760681c236" FOREIGN KEY ("messagesId") REFERENCES public.messages(id) ON UPDATE CASCADE ON DELETE CASCADE;`);
+ await queryRunner.query(`ALTER TABLE ONLY public.applications ADD CONSTRAINT "FK_a36ed02953077f408d0f3ebc424" FOREIGN KEY (team_id) REFERENCES public.teams(id) ON DELETE CASCADE;`);
+ await queryRunner.query(`ALTER TABLE ONLY public.message_role_mentions ADD CONSTRAINT "FK_a8242cf535337a490b0feaea0b4" FOREIGN KEY ("messagesId") REFERENCES public.messages(id) ON UPDATE CASCADE ON DELETE CASCADE;`);
+ await queryRunner.query(`ALTER TABLE ONLY public.messages ADD CONSTRAINT "FK_b0525304f2262b7014245351c76" FOREIGN KEY (member_id) REFERENCES public.users(id) ON DELETE CASCADE;`);
+ await queryRunner.query(`ALTER TABLE ONLY public.messages ADD CONSTRAINT "FK_b193588441b085352a4c0109423" FOREIGN KEY (guild_id) REFERENCES public.guilds(id) ON DELETE CASCADE;`);
+ await queryRunner.query(`ALTER TABLE ONLY public.message_user_mentions ADD CONSTRAINT "FK_b831eb18ceebd28976239b1e2f8" FOREIGN KEY ("usersId") REFERENCES public.users(id) ON UPDATE CASCADE ON DELETE CASCADE;`);
+ await queryRunner.query(`ALTER TABLE ONLY public.audit_logs ADD CONSTRAINT "FK_bd2726fd31b35443f2245b93ba0" FOREIGN KEY (user_id) REFERENCES public.users(id);`);
+ await queryRunner.query(`ALTER TABLE ONLY public.message_channel_mentions ADD CONSTRAINT "FK_bdb8c09e1464cabf62105bf4b9d" FOREIGN KEY ("channelsId") REFERENCES public.channels(id) ON UPDATE CASCADE ON DELETE CASCADE;`);
+ await queryRunner.query(`ALTER TABLE ONLY public.channels ADD CONSTRAINT "FK_c253dafe5f3a03ec00cd8fb4581" FOREIGN KEY (guild_id) REFERENCES public.guilds(id) ON DELETE CASCADE;`);
+ await queryRunner.query(`ALTER TABLE ONLY public.team_members ADD CONSTRAINT "FK_c2bf4967c8c2a6b845dadfbf3d4" FOREIGN KEY (user_id) REFERENCES public.users(id) ON DELETE CASCADE;`);
+ await queryRunner.query(`ALTER TABLE ONLY public.roles ADD CONSTRAINT "FK_c32c1ab1c4dc7dcb0278c4b1b8b" FOREIGN KEY (guild_id) REFERENCES public.guilds(id) ON DELETE CASCADE;`);
+ await queryRunner.query(`ALTER TABLE ONLY public.webhooks ADD CONSTRAINT "FK_c3e5305461931763b56aa905f1c" FOREIGN KEY (application_id) REFERENCES public.applications(id) ON DELETE CASCADE;`);
+ await queryRunner.query(`ALTER TABLE ONLY public.guilds ADD CONSTRAINT "FK_cfc3d3ad260f8121c95b31a1fce" FOREIGN KEY (system_channel_id) REFERENCES public.channels(id);`);
+ await queryRunner.query(`ALTER TABLE ONLY public.templates ADD CONSTRAINT "FK_d7374b7f8f5fbfdececa4fb62e1" FOREIGN KEY (creator_id) REFERENCES public.users(id);`);
+ await queryRunner.query(`ALTER TABLE ONLY public.webhooks ADD CONSTRAINT "FK_df528cf77e82f8032230e7e37d8" FOREIGN KEY (channel_id) REFERENCES public.channels(id) ON DELETE CASCADE;`);
+ await queryRunner.query(`ALTER TABLE ONLY public.message_stickers ADD CONSTRAINT "FK_e22a70819d07659c7a71c112a1f" FOREIGN KEY ("stickersId") REFERENCES public.stickers(id) ON UPDATE CASCADE ON DELETE CASCADE;`);
+ await queryRunner.query(`ALTER TABLE ONLY public.guilds ADD CONSTRAINT "FK_e2a2f873a64a5cf62526de42325" FOREIGN KEY (template_id) REFERENCES public.templates(id);`);
+ await queryRunner.query(`ALTER TABLE ONLY public.applications ADD CONSTRAINT "FK_e57508958bf92b9d9d25231b5e8" FOREIGN KEY (owner_id) REFERENCES public.users(id) ON DELETE CASCADE;`);
+ await queryRunner.query(`ALTER TABLE ONLY public.stickers ADD CONSTRAINT "FK_e7cfa5cefa6661b3fb8fda8ce69" FOREIGN KEY (pack_id) REFERENCES public.sticker_packs(id) ON DELETE CASCADE;`);
+ await queryRunner.query(`ALTER TABLE ONLY public.member_roles ADD CONSTRAINT "FK_e9080e7a7997a0170026d5139c1" FOREIGN KEY (role_id) REFERENCES public.roles(id) ON UPDATE CASCADE ON DELETE CASCADE;`);
+ await queryRunner.query(`ALTER TABLE ONLY public.connected_accounts ADD CONSTRAINT "FK_f47244225a6a1eac04a3463dd90" FOREIGN KEY (user_id) REFERENCES public.users(id) ON DELETE CASCADE;`);
+ await queryRunner.query(`ALTER TABLE ONLY public.guilds ADD CONSTRAINT "FK_f591a66b8019d87b0fe6c12dad6" FOREIGN KEY (afk_channel_id) REFERENCES public.channels(id);`);
+ await queryRunner.query(`ALTER TABLE ONLY public.messages ADD CONSTRAINT "FK_f83c04bcf1df4e5c0e7a52ed348" FOREIGN KEY (webhook_id) REFERENCES public.webhooks(id);`);
+ await queryRunner.query(`ALTER TABLE ONLY public.notes ADD CONSTRAINT "FK_f9e103f8ae67cb1787063597925" FOREIGN KEY (owner_id) REFERENCES public.users(id) ON DELETE CASCADE;`);
+ await queryRunner.query(`ALTER TABLE ONLY public.emojis ADD CONSTRAINT "FK_fa7ddd5f9a214e28ce596548421" FOREIGN KEY (user_id) REFERENCES public.users(id);`);
+ await queryRunner.query(`ALTER TABLE ONLY public.guilds ADD CONSTRAINT "FK_fc1a451727e3643ca572a3bb394" FOREIGN KEY (owner_id) REFERENCES public.users(id);`);
+ await queryRunner.query(`ALTER TABLE ONLY public.team_members ADD CONSTRAINT "FK_fdad7d5768277e60c40e01cdcea" FOREIGN KEY (team_id) REFERENCES public.teams(id) ON DELETE CASCADE;`);
+ }
+
+ public async down(queryRunner: QueryRunner): Promise {
+ throw new Error("Can't revert this: just throw away your database lol");
+ }
+}
+
diff --git a/src/util/migration/postgres/1696420827239-guildChannelOrdering.ts b/src/util/migration/postgres/1696420827239-guildChannelOrdering.ts
index 6fc80ffd6..1623f983e 100644
--- a/src/util/migration/postgres/1696420827239-guildChannelOrdering.ts
+++ b/src/util/migration/postgres/1696420827239-guildChannelOrdering.ts
@@ -34,7 +34,29 @@ export class guildChannelOrdering1696420827239 implements MigrationInterface {
await queryRunner.query(`ALTER TABLE channels DROP COLUMN position`);
}
- public async down(): Promise {
- // don't care actually, sorry.
+ public async down(queryRunner: QueryRunner): Promise {
+ await queryRunner.query(`ALTER TABLE channels ADD position integer NOT NULL DEFAULT 0`);
+
+ const guilds = await queryRunner.query(
+ `SELECT id, channel_ordering FROM guilds`,
+ undefined,
+ true,
+ );
+
+ for (const guild of guilds.records) {
+ const channel_ordering: string[] = JSON.parse(guild.channel_ordering);
+
+ for (let i = 0; i < channel_ordering.length; i++) {
+ const channel_id = channel_ordering[i];
+ await queryRunner.query(
+ `UPDATE channels SET position = $1 WHERE id = $2`,
+ [i, channel_id],
+ );
+ }
+ }
+
+ await queryRunner.query(
+ `ALTER TABLE guilds DROP COLUMN channel_ordering`,
+ );
}
}
diff --git a/src/util/migration/postgres/1713116476900-messageFlagsNotNull.ts b/src/util/migration/postgres/1713116476900-messageFlagsNotNull.ts
index 026b069b9..543a7ce38 100644
--- a/src/util/migration/postgres/1713116476900-messageFlagsNotNull.ts
+++ b/src/util/migration/postgres/1713116476900-messageFlagsNotNull.ts
@@ -16,8 +16,16 @@ export class MessageFlagsNotNull1713116476900 implements MigrationInterface {
await queryRunner.query("ALTER TABLE messages DROP COLUMN flags_old;");
}
- public async down(): Promise {
- // dont care
- throw new Error("Migration down is not implemented.");
+ public async down(queryRunner: QueryRunner): Promise {
+ await queryRunner.query(
+ "ALTER TABLE messages RENAME COLUMN flags TO flags_new;",
+ );
+ await queryRunner.query(
+ "ALTER TABLE messages ADD COLUMN flags integer;",
+ );
+ await queryRunner.query(
+ "UPDATE messages SET flags = flags_new;",
+ );
+ await queryRunner.query("ALTER TABLE messages DROP COLUMN flags_new;");
}
}
diff --git a/src/util/migration/postgres/1720628601997-badges.ts b/src/util/migration/postgres/1720628601997-badges.ts
index f7b9958bf..0cecb6ea1 100644
--- a/src/util/migration/postgres/1720628601997-badges.ts
+++ b/src/util/migration/postgres/1720628601997-badges.ts
@@ -12,5 +12,6 @@ export class Badges1720628601997 implements MigrationInterface {
public async down(queryRunner: QueryRunner): Promise {
await queryRunner.query(`ALTER TABLE "users" DROP COLUMN "badge_ids"`);
+ await queryRunner.query(`DROP TABLE "badges"`);
}
}
diff --git a/src/util/migration/postgres/1752321571508-RoleColors.ts b/src/util/migration/postgres/1752321571508-RoleColors.ts
index 687142e3c..beb42062f 100644
--- a/src/util/migration/postgres/1752321571508-RoleColors.ts
+++ b/src/util/migration/postgres/1752321571508-RoleColors.ts
@@ -5,6 +5,8 @@ export class RoleColors1752321571508 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise {
await queryRunner.query(`ALTER TABLE "roles" ADD "colors" text`);
+ await queryRunner.query(`UPDATE "roles" SET "colors" = jsonb_build_object('primary_color', "color") WHERE "colors" IS NULL`);
+ await queryRunner.query(`ALTER TABLE "roles" ALTER COLUMN "colors" SET NOT NULL`);
}
public async down(queryRunner: QueryRunner): Promise {
diff --git a/src/util/migration/postgres/1752342900886-RoleColorsSolidColor.ts b/src/util/migration/postgres/1752342900886-RoleColorsSolidColor.ts
deleted file mode 100644
index 36c24a375..000000000
--- a/src/util/migration/postgres/1752342900886-RoleColorsSolidColor.ts
+++ /dev/null
@@ -1,15 +0,0 @@
-import { MigrationInterface, QueryRunner } from "typeorm";
-
-export class RoleColorsSolidColor1752342900886 implements MigrationInterface {
- name = 'RoleColorsSolidColor1752342900886'
-
- public async up(queryRunner: QueryRunner): Promise {
- await queryRunner.query(`UPDATE "roles" SET "colors" = jsonb_build_object('primary_color', "color") WHERE "colors" IS NULL`);
- await queryRunner.query(`ALTER TABLE "roles" ALTER COLUMN "colors" SET NOT NULL`);
- }
-
- public async down(queryRunner: QueryRunner): Promise {
- await queryRunner.query(`ALTER TABLE "roles" ALTER COLUMN "colors" DROP NOT NULL`);
- }
-
-}
diff --git a/src/util/migration/postgres/1752383879533-message_pinned_at.ts b/src/util/migration/postgres/1752383879533-message_pinned_at.ts
index 2e294aadf..9f71f5fa6 100644
--- a/src/util/migration/postgres/1752383879533-message_pinned_at.ts
+++ b/src/util/migration/postgres/1752383879533-message_pinned_at.ts
@@ -14,12 +14,12 @@ export class MessagePinnedAt1752383879533 implements MigrationInterface {
}
public async down(queryRunner: QueryRunner): Promise {
- await queryRunner.query(
- `ALTER TABLE "messages" DROP COLUMN "pinned_at"`,
- );
await queryRunner.query(`ALTER TABLE "messages" ADD "pinned" boolean`);
await queryRunner.query(
`UPDATE "messages" SET "pinned" = true WHERE "pinned_at" IS NOT NULL`,
);
+ await queryRunner.query(
+ `ALTER TABLE "messages" DROP COLUMN "pinned_at"`,
+ );
}
}
diff --git a/src/util/migration/postgres/1761113394664-delete-bot-users-without-an-application.ts b/src/util/migration/postgres/1761113394664-delete-bot-users-without-an-application.ts
index 02f8a9340..e32099276 100644
--- a/src/util/migration/postgres/1761113394664-delete-bot-users-without-an-application.ts
+++ b/src/util/migration/postgres/1761113394664-delete-bot-users-without-an-application.ts
@@ -1,6 +1,7 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class DeleteBotUsersWithoutAnApplication1761113394664 implements MigrationInterface {
+ name = "DeleteBotUsersWithoutAnApplication1761113394664";
public async up(queryRunner: QueryRunner): Promise {
await queryRunner.query(`DELETE FROM users WHERE bot = true AND id NOT IN (SELECT bot_user_id FROM applications);`);
}
diff --git a/src/util/migration/postgres/1762611552514-fix-gif-stickers-format_type.ts b/src/util/migration/postgres/1762611552514-fix-gif-stickers-format_type.ts
index 47de75068..0e17bf814 100644
--- a/src/util/migration/postgres/1762611552514-fix-gif-stickers-format_type.ts
+++ b/src/util/migration/postgres/1762611552514-fix-gif-stickers-format_type.ts
@@ -1,6 +1,7 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class FixGifStickersFormatType1762611552514 implements MigrationInterface {
+ name = "FixGifStickersFormatType1762611552514";
public async up(queryRunner: QueryRunner): Promise {
await queryRunner.query(`UPDATE "stickers" SET "format_type" = 4 WHERE "format_type" = 0;`);
}
diff --git a/src/util/migration/postgres/1765578570423-InstanceBanTable.ts b/src/util/migration/postgres/1765578570423-InstanceBanTable.ts
new file mode 100644
index 000000000..0baa7dc9c
--- /dev/null
+++ b/src/util/migration/postgres/1765578570423-InstanceBanTable.ts
@@ -0,0 +1,16 @@
+import { MigrationInterface, QueryRunner } from "typeorm";
+
+export class InstanceBanTable1765578570423 implements MigrationInterface {
+ name = 'InstanceBanTable1765578570423'
+
+ public async up(queryRunner: QueryRunner): Promise {
+ await queryRunner.query(`CREATE TABLE "instance_bans" ("id" character varying NOT NULL, "created_at" TIMESTAMP NOT NULL DEFAULT now(), "reason" character varying NOT NULL, "user_id" character varying, "fingerprint" character varying, "ip_address" character varying, "is_from_other_instance_ban" boolean NOT NULL DEFAULT false, "origin_instance_ban_id" character varying, CONSTRAINT "REL_0b02d18d0d830f160c921192a3" UNIQUE ("origin_instance_ban_id"), CONSTRAINT "PK_3aa6e80a6d325601054892b1340" PRIMARY KEY ("id"))`);
+ await queryRunner.query(`ALTER TABLE "instance_bans" ADD CONSTRAINT "FK_0b02d18d0d830f160c921192a30" FOREIGN KEY ("origin_instance_ban_id") REFERENCES "instance_bans"("id") ON DELETE SET NULL ON UPDATE NO ACTION`);
+ }
+
+ public async down(queryRunner: QueryRunner): Promise {
+ await queryRunner.query(`ALTER TABLE "instance_bans" DROP CONSTRAINT "FK_0b02d18d0d830f160c921192a30"`);
+ await queryRunner.query(`DROP TABLE "instance_bans"`);
+ }
+
+}
diff --git a/src/util/migration/postgres/1765587835846-InstanceBanAllowlist.ts b/src/util/migration/postgres/1765587835846-InstanceBanAllowlist.ts
new file mode 100644
index 000000000..fd8da01ad
--- /dev/null
+++ b/src/util/migration/postgres/1765587835846-InstanceBanAllowlist.ts
@@ -0,0 +1,14 @@
+import { MigrationInterface, QueryRunner } from "typeorm";
+
+export class InstanceBanAllowlist1765587835846 implements MigrationInterface {
+ name = 'InstanceBanAllowlist1765587835846'
+
+ public async up(queryRunner: QueryRunner): Promise {
+ await queryRunner.query(`ALTER TABLE "instance_bans" ADD "is_allowlisted" boolean NOT NULL DEFAULT false`);
+ }
+
+ public async down(queryRunner: QueryRunner): Promise {
+ await queryRunner.query(`ALTER TABLE "instance_bans" DROP COLUMN "is_allowlisted"`);
+ }
+
+}
diff --git a/src/util/migration/postgres/1765665440000-DropDefaultIPDataKey.ts b/src/util/migration/postgres/1765665440000-DropDefaultIPDataKey.ts
new file mode 100644
index 000000000..ad1b736f7
--- /dev/null
+++ b/src/util/migration/postgres/1765665440000-DropDefaultIPDataKey.ts
@@ -0,0 +1,14 @@
+import { MigrationInterface, QueryRunner } from "typeorm";
+
+export class DropDefaultIPDataKey1765665440000 implements MigrationInterface {
+ name = 'DropDefaultIPDataKey1765665440000'
+
+ public async up(queryRunner: QueryRunner): Promise {
+ await queryRunner.query(`UPDATE "config" SET "value" = NULL WHERE "key" = 'security_ipdataApiKey' AND "value" = '"eca677b284b3bac29eb72f5e496aa9047f26543605efe99ff2ce35c9"'`);
+ }
+
+ public async down(queryRunner: QueryRunner): Promise {
+ await queryRunner.query(`UPDATE "config" SET "value" = '"eca677b284b3bac29eb72f5e496aa9047f26543605efe99ff2ce35c9"' WHERE "key" = 'security_ipdataApiKey' AND "value" IS NULL`);
+ }
+
+}
diff --git a/src/util/util/Database.ts b/src/util/util/Database.ts
index 50763dd9b..78712b9e7 100644
--- a/src/util/util/Database.ts
+++ b/src/util/util/Database.ts
@@ -22,6 +22,7 @@ import { green, red, yellow } from "picocolors";
import { DataSource } from "typeorm";
import { ConfigEntity } from "../entities/Config";
import { Migration } from "../entities/Migration";
+import fs from "fs";
// UUID extension option is only supported with postgres
// We want to generate all id's with Snowflakes that's why we have our own BaseEntity class
@@ -33,12 +34,9 @@ if (!process.env) {
config({ quiet: true });
}
-const dbConnectionString =
- process.env.DATABASE || path.join(process.cwd(), "database.db");
+const dbConnectionString = process.env.DATABASE || path.join(process.cwd(), "database.db");
-export const DatabaseType = dbConnectionString.includes("://")
- ? dbConnectionString.split(":")[0]?.replace("+srv", "")
- : "sqlite";
+export const DatabaseType = dbConnectionString.includes("://") ? dbConnectionString.split(":")[0]?.replace("+srv", "") : "sqlite";
const isSqlite = DatabaseType.includes("sqlite");
export const DataSourceOptions = new DataSource({
@@ -69,11 +67,7 @@ export async function initDatabase(): Promise {
if (dbConnection) return dbConnection;
if (isSqlite) {
- console.log(
- `[Database] ${red(
- `You are running sqlite! Please keep in mind that we recommend setting up a dedicated database!`,
- )}`,
- );
+ console.log(`[Database] ${red(`You are running sqlite! Please keep in mind that we recommend setting up a dedicated database!`)}`);
}
if (!process.env.DB_SYNC) {
@@ -94,6 +88,13 @@ export async function initDatabase(): Promise {
dbConnection = await DataSourceOptions.initialize();
+ if (DatabaseType === "sqlite") {
+ console.log(`[Database] ${yellow("Warning: SQLite is not supported. Forcing sync, this may lead to data loss!")}`);
+ await dbConnection.synchronize();
+ console.log(`[Database] ${green("Connected")}`);
+ return dbConnection;
+ }
+
// Crude way of detecting if the migrations table exists.
const dbExists = async () => {
try {
@@ -103,27 +104,17 @@ export async function initDatabase(): Promise {
return false;
}
};
- if (!(await dbExists())) {
- console.log(
- "[Database] This appears to be a fresh database. Synchronising.",
- );
- await dbConnection.synchronize();
- // On next start, typeorm will try to run all the migrations again from beginning.
- // Manually insert every current migration to prevent this:
- await Promise.all(
- dbConnection.migrations.map((migration) =>
- Migration.insert({
- name: migration.name,
- timestamp: Date.now(),
- }),
- ),
- );
- } else {
- console.log("[Database] Applying missing migrations, if any.");
- await dbConnection.runMigrations();
+ if (!(await dbExists())) {
+ console.log("[Database] This appears to be a fresh database. Running initial DDL.");
+ const qr = dbConnection.createQueryRunner();
+ if (fs.existsSync(path.join(__dirname, "..", "migration", DatabaseType, "initial0.js")))
+ await new (require(`../migration/${DatabaseType}-initial`).initial0)().up(qr);
}
+ console.log("[Database] Applying missing migrations, if any.");
+ await dbConnection.runMigrations();
+
console.log(`[Database] ${green("Connected")}`);
return dbConnection;
diff --git a/src/util/util/ElapsedTime.ts b/src/util/util/ElapsedTime.ts
index 518025c61..08f246a1a 100644
--- a/src/util/util/ElapsedTime.ts
+++ b/src/util/util/ElapsedTime.ts
@@ -68,4 +68,17 @@ export class ElapsedTime {
get days(): number {
return this.totalDays;
}
+
+ toString(): string {
+ // Format: "DD.HH:MM:SS.mmmuuuNNN", with days being optional
+ const daysPart = Math.floor(this.days) > 0 ? `${Math.floor(this.days)}.` : "";
+ const hoursPart = Math.floor(this.hours).toString().padStart(2, "0");
+ const minutesPart = Math.floor(this.minutes).toString().padStart(2, "0");
+ const secondsPart = Math.floor(this.seconds).toString().padStart(2, "0");
+ const millisecondsPart = Math.floor(this.milliseconds).toString().padStart(3, "0");
+ const microsecondsPart = Math.floor(this.microseconds).toString().padStart(3, "0");
+ const nanosecondsPart = Math.floor(this.nanoseconds).toString().padStart(3, "0");
+
+ return `${daysPart}${hoursPart}:${minutesPart}:${secondsPart}.${millisecondsPart}${microsecondsPart}${nanosecondsPart}`;
+ }
}
\ No newline at end of file
diff --git a/src/util/util/Event.ts b/src/util/util/Event.ts
index f56d66646..6f2b0d9a9 100644
--- a/src/util/util/Event.ts
+++ b/src/util/util/Event.ts
@@ -20,31 +20,28 @@ import { Channel } from "amqplib";
import { RabbitMQ } from "./RabbitMQ";
import EventEmitter from "events";
import { EVENT, Event } from "../interfaces";
+import { randomUUID } from "crypto";
export const events = new EventEmitter();
export async function emitEvent(payload: Omit) {
- const id = (payload.guild_id ||
- payload.channel_id ||
- payload.user_id) as string;
+ const id = (payload.guild_id || payload.channel_id || payload.user_id) as string;
if (!id) return console.error("event doesn't contain any id", payload);
if (RabbitMQ.connection) {
- const data =
- typeof payload.data === "object"
- ? JSON.stringify(payload.data)
- : payload.data; // use rabbitmq for event transmission
- await RabbitMQ.channel?.assertExchange(id, "fanout", {
- durable: false,
- });
+ const data = typeof payload.data === "object" ? JSON.stringify(payload.data) : payload.data; // use rabbitmq for event transmission
+ const channel = await RabbitMQ.getSafeChannel();
+ try {
+ await channel.assertExchange(id, "fanout", {
+ durable: false,
+ });
- // assertQueue isn't needed, because a queue will automatically created if it doesn't exist
- const successful = RabbitMQ.channel?.publish(
- id,
- "",
- Buffer.from(`${data}`),
- { type: payload.event },
- );
- if (!successful) throw new Error("failed to send event");
+ // assertQueue isn't needed, because a queue will automatically created if it doesn't exist
+ const successful = channel.publish(id, "", Buffer.from(`${data}`), { type: payload.event });
+ if (!successful) throw new Error("failed to send event");
+ } catch (e) {
+ // todo: should we retry publishng the event?
+ console.log("[RabbitMQ] ", e);
+ }
} else if (process.env.EVENT_TRANSMISSION === "process") {
process.send?.({ type: "event", event: payload, id } as ProcessEvent);
} else {
@@ -79,17 +76,11 @@ export interface ProcessEvent {
id: string;
}
-export async function listenEvent(
- event: string,
- callback: (event: EventOpts) => unknown,
- opts?: ListenEventOpts,
-) {
+export async function listenEvent(event: string, callback: (event: EventOpts) => unknown, opts?: ListenEventOpts): Promise<() => Promise> {
if (RabbitMQ.connection) {
- const channel = opts?.channel || RabbitMQ.channel;
- if (!channel)
- throw new Error(
- "[Events] An event was sent without an associated channel",
- );
+ const rabbitMQChannel = await RabbitMQ.getSafeChannel();
+ const channel = opts?.channel || rabbitMQChannel;
+ if (!channel) throw new Error("[Events] An event was sent without an associated channel");
return await rabbitListen(channel, event, callback, {
acknowledge: opts?.acknowledge,
});
@@ -101,9 +92,7 @@ export async function listenEvent(
const listener = (msg: ProcessEvent) => {
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
- msg.type === "event" &&
- msg.id === event &&
- callback({ ...msg.event, cancel });
+ msg.type === "event" && msg.id === event && callback({ ...msg.event, cancel });
};
// TODO: assert the type is correct?
@@ -124,20 +113,17 @@ export async function listenEvent(
}
}
-async function rabbitListen(
- channel: Channel,
- id: string,
- callback: (event: EventOpts) => unknown,
- opts?: { acknowledge?: boolean },
-) {
+async function rabbitListen(channel: Channel, id: string, callback: (event: EventOpts) => unknown, opts?: { acknowledge?: boolean }): Promise<() => Promise> {
await channel.assertExchange(id, "fanout", { durable: false });
const q = await channel.assertQueue("", {
exclusive: true,
autoDelete: true,
});
+ const consumerTag = randomUUID();
+
const cancel = async () => {
- await channel.cancel(q.queue);
+ await channel.cancel(consumerTag);
await channel.unbindQueue(q.queue, id, "");
};
@@ -163,6 +149,7 @@ async function rabbitListen(
},
{
noAck: !opts?.acknowledge,
+ consumerTag: consumerTag,
},
);
diff --git a/src/util/util/RabbitMQ.ts b/src/util/util/RabbitMQ.ts
index 1a61aee93..f11f701fb 100644
--- a/src/util/util/RabbitMQ.ts
+++ b/src/util/util/RabbitMQ.ts
@@ -23,6 +23,7 @@ export const RabbitMQ: {
connection: ChannelModel | null;
channel: Channel | null;
init: () => Promise;
+ getSafeChannel: () => Promise;
} = {
connection: null,
channel: null,
@@ -34,7 +35,38 @@ export const RabbitMQ: {
timeout: 1000 * 60,
});
console.log(`[RabbitMQ] connected`);
+
+ // log connection errors
+ this.connection.on("error", (err) => {
+ console.error("[RabbitMQ] Connection Error:", err);
+ });
+
+ this.connection.on("close", () => {
+ console.error("[RabbitMQ] connection closed");
+ // TODO: Add reconnection logic here if the connection crashes??
+ // will be a pain since we will have to reconstruct entire state
+ });
+
+ await this.getSafeChannel();
+ },
+ getSafeChannel: async function () {
+ if (!this.connection) return Promise.reject();
+
+ if (this.channel) return this.channel;
+
this.channel = await this.connection.createChannel();
console.log(`[RabbitMQ] channel created`);
+
+ // log channel errors
+ this.channel.on("error", (err) => {
+ console.error("[RabbitMQ] Channel Error:", err);
+ });
+
+ this.channel.on("close", () => {
+ console.log("[RabbitMQ] channel closed");
+ this.channel = null;
+ });
+
+ return this.channel;
},
};
diff --git a/src/util/util/Token.ts b/src/util/util/Token.ts
index 576349538..9e071ed98 100644
--- a/src/util/util/Token.ts
+++ b/src/util/util/Token.ts
@@ -18,15 +18,12 @@
import jwt, { VerifyOptions } from "jsonwebtoken";
import { Config } from "./Config";
-import { User } from "../entities";
+import { InstanceBan, 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 { FindManyOptions, FindOptions, FindOptionsRelationByString, FindOptionsSelect, FindOptionsSelectByString, FindOptionsWhere } from "typeorm";
import * as console from "node:console";
export const JWTOptions: VerifyOptions = { algorithms: ["HS256"] };
@@ -37,7 +34,7 @@ export type UserTokenData = {
};
function logAuth(text: string) {
- if(process.env.LOG_AUTH !== "true") return;
+ if (process.env.LOG_AUTH !== "true") return;
console.log(`[AUTH] ${text}`);
}
@@ -51,6 +48,8 @@ export const checkToken = (
opts?: {
select?: FindOptionsSelectByString;
relations?: FindOptionsRelationByString;
+ ipAddress?: string;
+ fingerprint?: string;
},
): Promise =>
new Promise((resolve, reject) => {
@@ -66,15 +65,7 @@ export const checkToken = (
const user = await User.findOne({
where: { id: decoded.id },
- select: [
- ...(opts?.select || []),
- "id",
- "bot",
- "disabled",
- "deleted",
- "rights",
- "data",
- ],
+ select: [...(opts?.select || []), "id", "bot", "disabled", "deleted", "rights", "data"],
relations: opts?.relations,
});
@@ -84,10 +75,7 @@ export const checkToken = (
}
// 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)
- ) {
+ 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");
}
@@ -96,11 +84,18 @@ export const checkToken = (
logAuth("validateUser rejected: User disabled");
return rejectAndLog(reject, "User disabled");
}
+
if (user.deleted) {
logAuth("validateUser rejected: User deleted");
return rejectAndLog(reject, "User not found");
}
+ const banReasons = await InstanceBan.findInstanceBans({ userId: user.id, ipAddress: opts?.ipAddress, fingerprint: opts?.fingerprint, propagateBan: true });
+ if (banReasons.length > 0) {
+ logAuth("validateUser rejected: User banned for reasons: " + banReasons.join(", "));
+ return rejectAndLog(reject, "Invalid Token");
+ }
+
logAuth("validateUser success: " + JSON.stringify({ decoded, user }));
return resolve({ decoded, user });
};
@@ -109,21 +104,11 @@ export const checkToken = (
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,
- );
+ if (dec.header.alg == "HS256" && Config.get().security.jwtSecret !== null) {
+ 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,
- );
+ jwt.verify(token, keyPair.publicKey, { algorithms: ["ES512"] }, validateUser);
});
} else return reject("Invalid token algorithm");
});
@@ -152,7 +137,7 @@ let cachedKeypair: {
privateKey: crypto.KeyObject;
publicKey: crypto.KeyObject;
fingerprint: string;
-}
+};
// Get ECDSA keypair from file or generate it
export async function loadOrGenerateKeypair() {
@@ -176,10 +161,7 @@ export async function loadOrGenerateKeypair() {
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"),
- ]);
+ const [loadedPrivateKey, loadedPublicKey] = await Promise.all([fs.readFile("jwt.key"), fs.readFile("jwt.key.pub")]);
privateKey = crypto.createPrivateKey(loadedPrivateKey);
publicKey = crypto.createPublicKey(loadedPublicKey);
@@ -192,14 +174,8 @@ export async function loadOrGenerateKeypair() {
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" }),
- ),
+ fs.writeFile("jwt.key", privateKey.export({ format: "pem", type: "sec1" })),
+ fs.writeFile("jwt.key.pub", publicKey.export({ format: "pem", type: "spki" })),
]);
}
@@ -209,5 +185,5 @@ export async function loadOrGenerateKeypair() {
.digest("hex");
lastFsCheck = Date.now();
- return cachedKeypair = { privateKey, publicKey, fingerprint };
+ return (cachedKeypair = { privateKey, publicKey, fingerprint });
}
diff --git a/src/util/util/WebAuthn.ts b/src/util/util/WebAuthn.ts
index a746f3646..d499097d9 100644
--- a/src/util/util/WebAuthn.ts
+++ b/src/util/util/WebAuthn.ts
@@ -19,13 +19,14 @@
import { Fido2Lib } from "fido2-lib";
import jwt from "jsonwebtoken";
import { Config } from "./Config";
+import { loadOrGenerateKeypair } from "./Token";
const jwtSignOptions: jwt.SignOptions = {
- algorithm: "HS256",
+ algorithm: "ES512",
expiresIn: "5m",
};
const jwtVerifyOptions: jwt.VerifyOptions = {
- algorithms: ["HS256"],
+ algorithms: ["ES512"],
};
export const WebAuthn: {
@@ -44,28 +45,32 @@ export async function generateWebAuthnTicket(
challenge: string,
): Promise {
return new Promise((res, rej) => {
- jwt.sign(
- { challenge },
- Config.get().security.jwtSecret,
- jwtSignOptions,
- (err, token) => {
- if (err || !token) return rej(err || "no token");
- return res(token);
- },
+ loadOrGenerateKeypair().then(kp=>
+ jwt.sign(
+ { challenge },
+ kp.privateKey,
+ jwtSignOptions,
+ (err, token) => {
+ if (err || !token) return rej(err || "no token");
+ return res(token);
+ },
+ )
);
});
}
export async function verifyWebAuthnToken(token: string) {
return new Promise((res, rej) => {
- jwt.verify(
- token,
- Config.get().security.jwtSecret,
- jwtVerifyOptions,
- async (err, decoded) => {
- if (err) return rej(err);
- return res(decoded);
- },
+ loadOrGenerateKeypair().then(kp=>
+ jwt.verify(
+ token,
+ kp.publicKey,
+ jwtVerifyOptions,
+ async (err, decoded) => {
+ if (err) return rej(err);
+ return res(decoded);
+ },
+ )
);
});
}