diff --git a/extra/admin-api/Spacebar.UApi/Controllers/GuildTemplatesController.cs b/extra/admin-api/Spacebar.UApi/Controllers/GuildTemplatesController.cs
index 6206a6c4..270b5543 100644
--- a/extra/admin-api/Spacebar.UApi/Controllers/GuildTemplatesController.cs
+++ b/extra/admin-api/Spacebar.UApi/Controllers/GuildTemplatesController.cs
@@ -8,9 +8,9 @@ namespace Spacebar.UApi.Controllers;
[ApiController]
public class GuildTemplatesController(ILogger<GuildMembersController> logger, SpacebarDbContext db, SpacebarAspNetAuthenticationService authService, TemplateImportService importService) : ControllerBase {
- [HttpPost("/api/v10/guilds/templates/{templateId}")]
- public async Task UseTemplate(string templateId, UseGuildTemplateRequest request) {
- var user = await authService.GetCurrentUserAsync(Request);
- await importService.CreateGuildFromTemplateById(templateId, request, user);
- }
+ // [HttpPost("/api/v10/guilds/templates/{templateId}")]
+ // public async Task UseTemplate(string templateId, UseGuildTemplateRequest request) {
+ // var user = await authService.GetCurrentUserAsync(Request);
+ // await importService.CreateGuildFromTemplateById(templateId, request, user);
+ // }
}
\ No newline at end of file
diff --git a/extra/admin-api/Spacebar.UApi/Program.cs b/extra/admin-api/Spacebar.UApi/Program.cs
index d6bb0d96..0a2a1b32 100644
--- a/extra/admin-api/Spacebar.UApi/Program.cs
+++ b/extra/admin-api/Spacebar.UApi/Program.cs
@@ -1,6 +1,10 @@
+using System.Text.Json;
using System.Text.Json.Serialization;
using ArcaneLibs;
+using ArcaneLibs.Extensions;
using Microsoft.EntityFrameworkCore;
+using Spacebar.ConfigModel;
+using Spacebar.ConfigModel.Extensions;
using Spacebar.Interop.Authentication;
using Spacebar.Interop.Authentication.AspNetCore;
using Spacebar.Models.Db.Contexts;
@@ -30,6 +34,25 @@ builder.Services.AddDbContextPool<SpacebarDbContext>(options => {
.EnableDetailedErrors();
});
+if (!string.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable("CONFIG_PATH")))
+ builder.Services.AddSingleton<Config>(sp => {
+ var cfgPath = Environment.GetEnvironmentVariable("CONFIG_PATH");
+ sp.GetService<ILogger<Program>>().LogInformation("Using config from path: {path}", cfgPath);
+ return JsonSerializer.Deserialize<Config>(File.ReadAllText(cfgPath));
+ });
+else
+ builder.Services.AddSingleton<Config>(sp => {
+ sp.GetService<ILogger<Program>>().LogInformation("Using config from database...");
+ var db = sp.GetService<SpacebarDbContext>();
+ var config = db.Configs
+ .OrderBy(x => x.Key)
+ .ToDictionary(x => x.Key, x => x.Value);
+
+ var readConfig = config.ToNestedJsonObject();
+ return readConfig.Deserialize<Config>();
+ });
+
+builder.Services.AddSingleton<UApiConfiguration>();
builder.Services.AddSingleton<SpacebarAuthenticationConfiguration>();
builder.Services.AddScoped<SpacebarAuthenticationService>();
builder.Services.AddScoped<SpacebarAspNetAuthenticationService>();
@@ -45,6 +68,7 @@ if (app.Environment.IsDevelopment()) {
app.UseAuthorization();
app.MapControllers();
+StreamingHttpClient.LogRequests = false;
app.Use((context, next) => {
context.Response.Headers["Access-Control-Allow-Origin"] = "*";
context.Response.Headers["Access-Control-Allow-Methods"] = "GET, POST, PUT, DELETE, OPTIONS";
@@ -57,13 +81,38 @@ app.Use((context, next) => {
return next();
});
+// add some special sauce
+app.Map("/", async context => {
+ var client = new StreamingHttpClient();
+ var cfg = context.RequestServices.GetService<UApiConfiguration>();
+ var requestMessage = new HttpRequestMessage(new HttpMethod(context.Request.Method), cfg.FallbackApiEndpoint);
+
+ foreach (var header in context.Request.Headers)
+ if (header.Key is not ("Accept-Encoding" or "Host"))
+ requestMessage.Headers.TryAddWithoutValidation(header.Key, header.Value.ToArray());
+
+ var responseMessage = await client.SendUnhandledAsync(requestMessage, CancellationToken.None);
+ context.Response.StatusCode = (int)responseMessage.StatusCode;
+
+ foreach (var header in responseMessage.Headers) context.Response.Headers[header.Key] = header.Value.ToArray();
+ foreach (var header in responseMessage.Content.Headers) context.Response.Headers[header.Key] = header.Value.ToArray();
+
+ // await responseMessage.Content.CopyToAsync(context.Response.Body);
+ var txt = await responseMessage.Content.ReadAsStringAsync();
+ txt = txt.Replace("your very own Spacebar instance", "your very own Spacebar instance with μAPI");
+ var data = txt.AsBytes().ToArray();
+ context.Response.Headers.ContentLength = data.Length;
+ await context.Response.Body.WriteAsync(data);
+});
+
// fallback to proxy in case we dont have a specific endpoint...
// TODO config
app.MapFallback("{*_}", async context => {
var client = new StreamingHttpClient();
+ var cfg = context.RequestServices.GetService<UApiConfiguration>();
var requestMessage = new HttpRequestMessage(
new HttpMethod(context.Request.Method),
- "http://api.old.server.spacebar.chat" + context.Request.Path + context.Request.QueryString
+ cfg.FallbackApiEndpoint + context.Request.Path + context.Request.QueryString
) {
Content = new StreamContent(context.Request.Body)
};
diff --git a/extra/admin-api/Spacebar.UApi/Services/UApiConfiguration.cs b/extra/admin-api/Spacebar.UApi/Services/UApiConfiguration.cs
new file mode 100644
index 00000000..84b01490
--- /dev/null
+++ b/extra/admin-api/Spacebar.UApi/Services/UApiConfiguration.cs
@@ -0,0 +1,10 @@
+namespace Spacebar.UApi.Services;
+
+public class UApiConfiguration {
+ public UApiConfiguration(IConfiguration config) {
+ config.GetRequiredSection("Spacebar").GetRequiredSection("UApi").Bind(this);
+ }
+
+ // ... for unhandled routes
+ public string? FallbackApiEndpoint { get; set; }
+}
\ No newline at end of file
diff --git a/extra/admin-api/Spacebar.UApi/appsettings.Development.json b/extra/admin-api/Spacebar.UApi/appsettings.Development.json
index 0c208ae9..524b10da 100644
--- a/extra/admin-api/Spacebar.UApi/appsettings.Development.json
+++ b/extra/admin-api/Spacebar.UApi/appsettings.Development.json
@@ -4,5 +4,17 @@
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
+ },
+ "ConnectionStrings": {
+ "Spacebar": "Host=127.0.0.1; Username=postgres; Database=spacebar; Port=5433; Include Error Detail=true; Maximum Pool Size=1000; Command Timeout=6000; Timeout=600;"
+ },
+ "Spacebar": {
+ "Authentication": {
+ "PublicKeyPath": "../../../jwt.key.pub",
+ "PrivateKeyPath": "../../../jwt.key"
+ },
+ "UApi":{
+ "FallbackApiEndpoint": "http://localhost:3113"
+ }
}
}
diff --git a/extra/admin-api/Spacebar.UApi/deps.json b/extra/admin-api/Spacebar.UApi/deps.json
new file mode 100644
index 00000000..fe51488c
--- /dev/null
+++ b/extra/admin-api/Spacebar.UApi/deps.json
@@ -0,0 +1 @@
+[]
diff --git a/extra/admin-api/outputs.nix b/extra/admin-api/outputs.nix
index 43430100..fa80b39b 100644
--- a/extra/admin-api/outputs.nix
+++ b/extra/admin-api/outputs.nix
@@ -218,6 +218,22 @@ flake-utils.lib.eachSystem flake-utils.lib.allSystems (
proj.Spacebar-Models-Generic
];
};
+ Spacebar-UApi = makeNupkg {
+ name = "Spacebar.UApi";
+ nugetDeps = Spacebar.UApi/deps.json;
+ projectFile = "Spacebar.UApid.csproj";
+ srcRoot = ./Spacebar.UApi;
+ packNupkg = false;
+ projectReferences = [
+ proj.Spacebar-DataMappings-Generic
+ proj.Spacebar-Interop-Authentication
+ proj.Spacebar-Interop-Authentication-AspNetCore
+ proj.Spacebar-Interop-Replication-Abstractions
+ proj.Spacebar-Models-Db
+ proj.Spacebar-Models-Gateway
+ proj.Spacebar-Models-Generic
+ ];
+ };
# Spacebar-AdminApi-TestClient = makeNupkg {
# name = "Spacebar.AdminApi.TestClient";
# projectFile = "Utilities/Spacebar.AdminApi.TestClient/Spacebar.AdminApi.TestClient.csproj";
@@ -257,6 +273,15 @@ flake-utils.lib.eachSystem flake-utils.lib.allSystems (
Expose = [ "5000" ];
};
};
+ containers.docker.uapi = pkgs.dockerTools.buildLayeredImage {
+ name = "spacebar-server-ts-cdn-cs";
+ tag = builtins.replaceStrings [ "+" ] [ "_" ] self.packages.${system}.Spacebar-AdminApi.version;
+ contents = [ self.packages.${system}.Spacebar-AdminApi ];
+ config = {
+ Cmd = [ "${lib.getExe self.outputs.packages.${system}.Spacebar-AdminApi}" ];
+ Expose = [ "5000" ];
+ };
+ };
}
)
// {
diff --git a/nix/modules/default/cs/uapi.nix b/nix/modules/default/cs/uapi.nix
new file mode 100644
index 00000000..262ce6ba
--- /dev/null
+++ b/nix/modules/default/cs/uapi.nix
@@ -0,0 +1,163 @@
+self:
+{
+ config,
+ lib,
+ pkgs,
+ spacebar,
+ ...
+}:
+
+let
+ secrets = import ../secrets.nix { inherit lib config; };
+ cfg = config.services.spacebarchat-server;
+ jsonFormat = pkgs.formats.json { };
+in
+{
+ imports = [ ];
+ options.services.spacebarchat-server.uApi = lib.mkOption {
+ default = { };
+ description = "Configuration for C# API overlay.";
+ type = lib.types.submodule {
+ options = {
+ enable = lib.mkEnableOption "Enable C# API overlay.";
+ extraConfiguration = lib.mkOption {
+ type = jsonFormat.type;
+ default = import ./default-appsettings-json.nix;
+ description = "Extra appsettings.json configuration for the C# API overlay.";
+ };
+ };
+ };
+ };
+
+ config = lib.mkIf cfg.uApi.enable (
+ let
+ makeServerTsService = (
+ conf:
+ lib.recursiveUpdate
+ (lib.recursiveUpdate {
+ documentation = [ "https://docs.spacebar.chat/" ];
+ wantedBy = [ "multi-user.target" ];
+ wants = [ "network-online.target" ];
+ after = [ "network-online.target" ];
+ environment = secrets.systemdEnvironment;
+ serviceConfig = {
+ LoadCredential = secrets.systemdLoadCredentials;
+
+ User = "spacebarchat";
+ Group = "spacebarchat";
+ DynamicUser = false;
+
+ LockPersonality = true;
+ NoNewPrivileges = true;
+
+ ProtectClock = true;
+ ProtectControlGroups = true;
+ ProtectHostname = true;
+ ProtectKernelLogs = true;
+ ProtectKernelModules = true;
+ ProtectKernelTunables = true;
+ PrivateDevices = true;
+ PrivateMounts = true;
+ PrivateUsers = true;
+ RestrictAddressFamilies = [
+ "AF_INET"
+ "AF_INET6"
+ "AF_UNIX"
+ ];
+ RestrictNamespaces = true;
+ RestrictRealtime = true;
+ SystemCallArchitectures = "native";
+ SystemCallFilter = [
+ "@system-service"
+ "~@privileged"
+ "@chown" # Required for copying files with FICLONE, apparently.
+ ];
+ CapabilityBoundingSet = [
+ "~CAP_SYS_ADMIN"
+ "~CAP_AUDIT_*"
+ "~CAP_NET_(BIND_SERVICE|BROADCAST|RAW)"
+ "~CAP_NET_ADMIN" # No use for this as we don't currently use iptables for enforcing instance bans
+ "~CAP_SYS_TIME"
+ "~CAP_KILL"
+ "~CAP_(DAC_*|FOWNER|IPC_OWNER)"
+ "~CAP_LINUX_IMMUTABLE"
+ "~CAP_IPC_LOCK"
+ "~CAP_BPF"
+ "~CAP_SYS_TTY_CONFIG"
+ "~CAP_SYS_BOOT"
+ "~CAP_SYS_CHROOT"
+ "~CAP_BLOCK_SUSPEND"
+ "~CAP_LEASE"
+ "~CAP_(CHOWN|FSETID|FSETFCAP)" # Check if we need CAP_CHOWN for `fchown()` (FICLONE)?
+ "~CAP_SET(UID|GID|PCAP)"
+ "~CAP_MAC_*"
+ "~CAP_SYS_PTRACE"
+ "~CAP_SYS_(NICE|RESOURCE)"
+ "~CAP_SYS_RAWIO"
+ "~CAP_SYSLOG"
+ ];
+ RestrictSUIDSGID = true;
+
+ WorkingDirectory = "/var/lib/spacebar";
+ StateDirectory = "spacebar";
+ StateDirectoryMode = "0750";
+ RuntimeDirectory = "spacebar";
+ RuntimeDirectoryMode = "0750";
+ ReadWritePaths = [ cfg.cdnPath ];
+ NoExecPaths = [ cfg.cdnPath ];
+
+ Restart = "on-failure";
+ RestartSec = 10;
+ StartLimitBurst = 5;
+ UMask = "077";
+ }
+ // lib.optionalAttrs (cfg.databaseFile != null) { EnvironmentFile = cfg.databaseFile; };
+ } conf)
+ {
+ }
+ );
+ in
+ {
+ assertions = [
+ {
+ assertion =
+ cfg.adminApi.extraConfiguration ? ConnectionStrings
+ && cfg.adminApi.extraConfiguration.ConnectionStrings ? Spacebar
+ && cfg.adminApi.extraConfiguration.ConnectionStrings.Spacebar != null;
+ message = ''
+ Admin API: Setting a database connection string in extraConfiguration (`extraConfiguration.ConnectionStrings.Spacebar`) is required when using C# services.
+ Example: Host=127.0.0.1; Username=Spacebar; Password=SuperSecurePassword12; Database=spacebar; Port=5432; Include Error Detail=true; Maximum Pool Size=1000; Command Timeout=6000; Timeout=600;
+ '';
+ }
+ ];
+
+ services.spacebarchat-server.settings.admin = {
+ endpointPublic = "http${if cfg.adminApiEndpoint.useSsl then "s" else ""}://${cfg.adminApiEndpoint.host}:${toString cfg.adminApiEndpoint.publicPort}";
+ endpointPrivate = "http://127.0.0.1:${builtins.toString cfg.adminApiEndpoint.localPort}";
+ };
+
+ systemd.services.spacebar-admin-api = makeServerTsService {
+ description = "Spacebar Server - Admin API";
+ environment = builtins.mapAttrs (_: val: builtins.toString val) (
+ {
+ # things we set by default...
+ EVENT_TRANSMISSION = "unix";
+ EVENT_SOCKET_PATH = "/run/spacebar/";
+ }
+ // cfg.extraEnvironment
+ // {
+ # things we force...
+ # CONFIG_PATH = configFile;
+ CONFIG_READONLY = 1;
+ ASPNETCORE_URLS = "http://0.0.0.0:${toString cfg.uApi.localPort}";
+ STORAGE_LOCATION = cfg.cdnPath;
+ APPSETTINGS_PATH = jsonFormat.generate "appsettings.spacebar-uapi.json" (lib.recursiveUpdate (import ./default-appsettings-json.nix) cfg.uApi.extraConfiguration);
+ }
+ );
+ serviceConfig = {
+ ExecStart = "${self.packages.${pkgs.stdenv.hostPlatform.system}.Spacebar-UApi}/bin/Spacebar.UApi";
+ };
+ };
+ }
+ );
+}
diff --git a/nix/modules/default/default.nix b/nix/modules/default/default.nix
index ac20d3cd..8306aba6 100644
--- a/nix/modules/default/default.nix
+++ b/nix/modules/default/default.nix
@@ -47,6 +47,7 @@ in
./users.nix
(import ./cs/gateway-offload-cs.nix self)
(import ./cs/admin-api.nix self)
+ (import ./cs/uapi.nix self)
];
options.services.spacebarchat-server =
let
|