Server updates from the web panel with progress screen
- Updater checks git.mbm.wtf every 6 h (and on demand) for a newer <Version> on main, shows the new commits and the required mod version - Install: download the source, build it with dotnet publish into data/app/current, warn players and restart (exit code 75 for Docker/systemd, self-relaunch on Windows). At startup a newer installed update is loaded in its own AssemblyLoadContext; after 3 failed starts the built-in version runs - Panel: Update page, sidebar badge, full-screen progress overlay (download, build, install, restart) that survives the restart and reloads the panel - Panel sessions survive restarts (hashed tokens in data/panel_sessions.json) - Docker image is based on the .NET SDK so it can build updates - Version 2.9.0 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+65
-5
@@ -60,6 +60,7 @@ public sealed class AdminPanel
|
||||
this.dataDir = dataDir;
|
||||
this.config = config;
|
||||
users = new PanelUsers(Path.Combine(dataDir, "panel_users.json"));
|
||||
LoadSessions();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- Start
|
||||
@@ -357,7 +358,7 @@ public sealed class AdminPanel
|
||||
{
|
||||
if (!CsrfOk(ctx)) return Forbidden("Missing header");
|
||||
string token = ctx.Request.Cookies[CookieName];
|
||||
if (token != null) sessions.TryRemove(token, out _);
|
||||
if (token != null && sessions.TryRemove(TokenKey(token), out _)) SaveSessions();
|
||||
ctx.Response.Cookies.Delete(CookieName, CookieOptions(ctx));
|
||||
return Results.Json(new { ok = true });
|
||||
});
|
||||
@@ -527,6 +528,25 @@ public sealed class AdminPanel
|
||||
};
|
||||
});
|
||||
|
||||
// --- Updates (prüfen, installieren, Fortschritt)
|
||||
app.MapGet("/api/update", (HttpContext ctx) =>
|
||||
{
|
||||
var user = Require(ctx, PanelRole.Moderator, out var denied); if (user == null) return denied;
|
||||
return Results.Json(host.Updater.Status());
|
||||
});
|
||||
app.MapPost("/api/update/check", async (HttpContext ctx) =>
|
||||
{
|
||||
var user = Require(ctx, PanelRole.Admin, out var denied); if (user == null) return denied;
|
||||
await host.Updater.CheckAsync();
|
||||
return Results.Json(host.Updater.Status());
|
||||
});
|
||||
app.MapPost("/api/update/install", (HttpContext ctx) =>
|
||||
{
|
||||
var user = Require(ctx, PanelRole.Admin, out var denied); if (user == null) return denied;
|
||||
string problem = host.Updater.Install(By(user));
|
||||
return problem != null ? Error(409, problem) : Ok("Update started");
|
||||
});
|
||||
|
||||
// --- Einstellungen (server.json)
|
||||
app.MapGet("/api/config", async (HttpContext ctx) =>
|
||||
{
|
||||
@@ -740,11 +760,14 @@ public sealed class AdminPanel
|
||||
PanelUsers.User CurrentUser(HttpContext ctx)
|
||||
{
|
||||
string token = ctx.Request.Cookies[CookieName];
|
||||
if (token == null || !sessions.TryGetValue(token, out var session)) return null;
|
||||
if (session.Expires < DateTime.UtcNow) { sessions.TryRemove(token, out _); return null; }
|
||||
if (token == null) return null;
|
||||
string key = TokenKey(token);
|
||||
if (!sessions.TryGetValue(key, out var session)) return null;
|
||||
if (session.Expires < DateTime.UtcNow) { sessions.TryRemove(key, out _); SaveSessions(); return null; }
|
||||
var user = users.Find(session.User);
|
||||
if (user == null) { sessions.TryRemove(token, out _); return null; }
|
||||
if (user == null) { sessions.TryRemove(key, out _); SaveSessions(); return null; }
|
||||
session.Expires = DateTime.UtcNow + SessionLifetime; // gleitend verlängern
|
||||
if (DateTime.UtcNow - lastSessionSave > TimeSpan.FromMinutes(5)) SaveSessions();
|
||||
return user;
|
||||
}
|
||||
|
||||
@@ -761,16 +784,53 @@ public sealed class AdminPanel
|
||||
void StartSession(HttpContext ctx, string name)
|
||||
{
|
||||
string token = Convert.ToBase64String(RandomNumberGenerator.GetBytes(32)).TrimEnd('=').Replace('+', '-').Replace('/', '_');
|
||||
sessions[token] = new Session { User = name, Expires = DateTime.UtcNow + SessionLifetime };
|
||||
sessions[TokenKey(token)] = new Session { User = name, Expires = DateTime.UtcNow + SessionLifetime };
|
||||
ctx.Response.Cookies.Append(CookieName, token, CookieOptions(ctx));
|
||||
// Alte Sitzungen aufräumen
|
||||
foreach (var kv in sessions) if (kv.Value.Expires < DateTime.UtcNow) sessions.TryRemove(kv.Key, out _);
|
||||
SaveSessions();
|
||||
}
|
||||
|
||||
void EndSessionsOf(string name)
|
||||
{
|
||||
foreach (var kv in sessions)
|
||||
if (string.Equals(kv.Value.User, name, StringComparison.OrdinalIgnoreCase)) sessions.TryRemove(kv.Key, out _);
|
||||
SaveSessions();
|
||||
}
|
||||
|
||||
// Sitzungen überstehen einen Neustart (z.B. nach einem Update). Gespeichert wird nur ein Hash des Cookies.
|
||||
static readonly JsonSerializerOptions SessionJson = new() { IncludeFields = true };
|
||||
readonly object sessionFileLock = new();
|
||||
DateTime lastSessionSave = DateTime.MinValue;
|
||||
string SessionsPath => Path.Combine(dataDir, "panel_sessions.json");
|
||||
|
||||
static string TokenKey(string token) => Convert.ToHexString(SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(token)));
|
||||
|
||||
void LoadSessions()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!File.Exists(SessionsPath)) return;
|
||||
var saved = JsonSerializer.Deserialize<Dictionary<string, Session>>(File.ReadAllText(SessionsPath), SessionJson);
|
||||
foreach (var kv in saved ?? new())
|
||||
if (kv.Value?.User != null && kv.Value.Expires > DateTime.UtcNow) sessions[kv.Key] = kv.Value;
|
||||
}
|
||||
catch { } // kaputte Datei: alle melden sich neu an
|
||||
}
|
||||
|
||||
void SaveSessions()
|
||||
{
|
||||
lock (sessionFileLock)
|
||||
{
|
||||
lastSessionSave = DateTime.UtcNow;
|
||||
try
|
||||
{
|
||||
string tmp = SessionsPath + ".tmp";
|
||||
File.WriteAllText(tmp, JsonSerializer.Serialize(sessions.ToDictionary(kv => kv.Key, kv => kv.Value), SessionJson));
|
||||
File.Move(tmp, SessionsPath, true);
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
}
|
||||
|
||||
static CookieOptions CookieOptions(HttpContext ctx) => new()
|
||||
|
||||
Reference in New Issue
Block a user