summary refs log tree commit diff
diff options
context:
space:
mode:
authorRory& <root@rory.gay>2026-02-20 03:19:22 +0100
committerRory& <root@rory.gay>2026-02-20 03:20:33 +0100
commitb6c36b0b21e284621d8839e1af5f9647a2108bbb (patch)
tree123531bfa0378253d30664d9f45732d9c6711022
parentAdmin API work (diff)
downloadserver-ts-b6c36b0b21e284621d8839e1af5f9647a2108bbb.tar.xz
UAPI work
-rw-r--r--extra/admin-api/Spacebar.UApi/Controllers/GuildTemplatesController.cs10
-rw-r--r--extra/admin-api/Spacebar.UApi/Program.cs51
-rw-r--r--extra/admin-api/Spacebar.UApi/Services/UApiConfiguration.cs10
-rw-r--r--extra/admin-api/Spacebar.UApi/appsettings.Development.json12
-rw-r--r--extra/admin-api/Spacebar.UApi/deps.json1
-rw-r--r--extra/admin-api/outputs.nix25
-rw-r--r--nix/modules/default/cs/uapi.nix163
-rw-r--r--nix/modules/default/default.nix1
8 files changed, 267 insertions, 6 deletions
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