diff --git a/CannedNet.Admin/Components/Pages/Settings.razor b/CannedNet.Admin/Components/Pages/Settings.razor
index 8f8245b..9019d2d 100644
--- a/CannedNet.Admin/Components/Pages/Settings.razor
+++ b/CannedNet.Admin/Components/Pages/Settings.razor
@@ -80,6 +80,7 @@
return;
}
+ edit.NsBaseUrl = edit.ServerBaseUrl;
AdminConfig.Save(edit);
saved = true;
}
diff --git a/CannedNet.Admin/Components/Pages/SetupWizard.razor b/CannedNet.Admin/Components/Pages/SetupWizard.razor
index 41ea9f9..330c706 100644
--- a/CannedNet.Admin/Components/Pages/SetupWizard.razor
+++ b/CannedNet.Admin/Components/Pages/SetupWizard.razor
@@ -2,6 +2,7 @@
@rendermode InteractiveServer
@inject AdminConfigService AdminConfig
@inject NavigationManager Navigation
+@using System.Net.Http.Json
First-Time Setup
@@ -60,6 +61,11 @@ else
private bool error;
private string errorMessage = "";
+ private class KeyResponse
+ {
+ public string? apiKey { get; set; }
+ }
+
private async Task Save()
{
saving = true;
@@ -83,7 +89,26 @@ else
return;
}
+ edit.NsBaseUrl = edit.ServerBaseUrl;
AdminConfig.Save(edit);
+
+ try
+ {
+ using var client = new HttpClient();
+ client.BaseAddress = new Uri(edit.ServerBaseUrl.TrimEnd('/'));
+
+ var keyResp = await client.GetFromJsonAsync("/admin/api/setup/key");
+ if (keyResp?.apiKey != null)
+ {
+ client.DefaultRequestHeaders.Add("X-Admin-Api-Key", keyResp.apiKey);
+ await client.PostAsJsonAsync("/admin/api/setup/baseurl", new { baseUrl = edit.ServerBaseUrl });
+ }
+ }
+ catch
+ {
+ // server config update is best-effort during setup
+ }
+
saved = true;
await Task.Delay(1000);
Navigation.NavigateTo("/login", forceLoad: true);
diff --git a/CannedNet.Admin/Services/AdminConfigService.cs b/CannedNet.Admin/Services/AdminConfigService.cs
index cd1701b..d47e5e9 100644
--- a/CannedNet.Admin/Services/AdminConfigService.cs
+++ b/CannedNet.Admin/Services/AdminConfigService.cs
@@ -42,5 +42,6 @@ public class AdminConfigService
public class AdminConfigData
{
public string ServerBaseUrl { get; set; } = "";
+ public string NsBaseUrl { get; set; } = "";
}
}
diff --git a/CannedNet.Admin/appsettings.json b/CannedNet.Admin/appsettings.json
index b28cc6d..ce20ac1 100644
--- a/CannedNet.Admin/appsettings.json
+++ b/CannedNet.Admin/appsettings.json
@@ -7,8 +7,5 @@
},
"AllowedHosts": "*",
"ServerBaseUrl": "http://localhost:5000",
- "ImagesBaseUrl": "https://cannednet.lapis.codes/img",
- "Admin": {
- "ApiKey": "changeme-replace-with-a-secure-key"
- }
+ "ImagesBaseUrl": "https://cannednet.lapis.codes/img"
}
diff --git a/CannedNet/Program.cs b/CannedNet/Program.cs
index be3145a..8e86fa1 100644
--- a/CannedNet/Program.cs
+++ b/CannedNet/Program.cs
@@ -1,4 +1,5 @@
using System.Security.Claims;
+using System.Security.Cryptography;
using System.Text.Json.Serialization;
using CannedNet.Data;
using CannedNet.Hubs;
@@ -42,6 +43,23 @@ public static class Program
builder.Services.AddSingleton();
builder.Services.AddSignalR();
+ var adminApiKey = builder.Configuration["Admin:ApiKey"];
+ if (string.IsNullOrEmpty(adminApiKey))
+ {
+ var keyFile = Path.Combine("Data", ".admin_key");
+ if (File.Exists(keyFile))
+ {
+ adminApiKey = File.ReadAllText(keyFile).Trim();
+ }
+ else
+ {
+ adminApiKey = Convert.ToHexString(RandomNumberGenerator.GetBytes(32));
+ Directory.CreateDirectory("Data");
+ File.WriteAllText(keyFile, adminApiKey);
+ }
+ builder.Configuration["Admin:ApiKey"] = adminApiKey;
+ }
+
builder.Services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
diff --git a/CannedNet/Services/Controllers/AdminController.cs b/CannedNet/Services/Controllers/AdminController.cs
index 4c1c1a8..e147904 100644
--- a/CannedNet/Services/Controllers/AdminController.cs
+++ b/CannedNet/Services/Controllers/AdminController.cs
@@ -360,3 +360,42 @@ public class AdminController : ControllerBase
return Results.Ok(new { success = true });
}
}
+
+[ApiController, Route("admin/api/setup")]
+public class SetupController : ControllerBase
+{
+ private static readonly string SetupCompleteFile = Path.Combine("Data", ".setup_complete");
+
+ [HttpGet("key")]
+ public IResult GetKey([FromServices] IConfiguration config)
+ {
+ if (System.IO.File.Exists(SetupCompleteFile))
+ return Results.NotFound();
+
+ var key = config["Admin:ApiKey"] ?? "";
+ return Results.Ok(new { apiKey = key });
+ }
+
+ [HttpPost("baseurl")]
+ [Authorize(AuthenticationSchemes = "AdminApiKey")]
+ public async Task SetBaseUrl([FromServices] ConfigService configService)
+ {
+ using var reader = new StreamReader(Request.Body);
+ var body = await reader.ReadToEndAsync();
+ var data = JsonSerializer.Deserialize(body);
+
+ if (!data.TryGetProperty("baseUrl", out var baseUrlEl) || baseUrlEl.ValueKind != JsonValueKind.String)
+ return Results.BadRequest(new { error = "baseUrl is required" });
+
+ var baseUrl = baseUrlEl.GetString() ?? "";
+ if (string.IsNullOrWhiteSpace(baseUrl))
+ return Results.BadRequest(new { error = "baseUrl cannot be empty" });
+
+ var current = configService.Config;
+ current.BaseUrl = baseUrl.TrimEnd('/');
+ configService.SaveConfig(current);
+ System.IO.File.WriteAllText(SetupCompleteFile, "done");
+
+ return Results.Ok(new { success = true });
+ }
+}