diff --git a/Content.Server/Administration/ServerAPI.cs b/Content.Server/Administration/ServerAPI.cs new file mode 100644 index 0000000000..27e9a83f47 --- /dev/null +++ b/Content.Server/Administration/ServerAPI.cs @@ -0,0 +1,795 @@ +using System.Net; +using System.Net.Http; +using System.Text.Json; +using System.Text.Json.Nodes; +using System.Threading.Tasks; +using Content.Server.Administration.Systems; +using Content.Server.GameTicking; +using Content.Server.GameTicking.Presets; +using Content.Server.GameTicking.Rules.Components; +using Content.Server.Maps; +using Content.Server.RoundEnd; +using Content.Shared.Administration.Managers; +using Content.Shared.CCVar; +using Content.Shared.Prototypes; +using Robust.Server.ServerStatus; +using Robust.Shared.Asynchronous; +using Robust.Shared.Configuration; +using Robust.Shared.Network; +using Robust.Shared.Player; +using Robust.Shared.Prototypes; +using Robust.Shared.Utility; + +namespace Content.Server.Administration; + +public sealed class ServerApi : IPostInjectInit +{ + [Dependency] private readonly IStatusHost _statusHost = default!; + [Dependency] private readonly IConfigurationManager _config = default!; + [Dependency] private readonly ISharedPlayerManager _playerManager = default!; // Players + [Dependency] private readonly ISharedAdminManager _adminManager = default!; // Admins + [Dependency] private readonly IGameMapManager _gameMapManager = default!; // Map name + [Dependency] private readonly IServerNetManager _netManager = default!; // Kick + [Dependency] private readonly IPrototypeManager _prototypeManager = default!; // Game rules + [Dependency] private readonly IComponentFactory _componentFactory = default!; // Needed to circumvent the "IoC has no context on this thread" error until I figure out how to do it properly + [Dependency] private readonly ITaskManager _taskManager = default!; // game explodes when calling stuff from the non-game thread + [Dependency] private readonly EntityManager _entityManager = default!; + + [Dependency] private readonly IEntitySystemManager _entitySystemManager = default!; + + private string _token = default!; + private ISawmill _sawmill = default!; + private string _motd = default!; + + public void PostInject() + { + _sawmill = Logger.GetSawmill("serverApi"); + + // Get + _statusHost.AddHandler(InfoHandler); + _statusHost.AddHandler(GetGameRules); + _statusHost.AddHandler(GetForcePresets); + + // Post + _statusHost.AddHandler(ActionRoundStatus); + _statusHost.AddHandler(ActionKick); + _statusHost.AddHandler(ActionAddGameRule); + _statusHost.AddHandler(ActionEndGameRule); + _statusHost.AddHandler(ActionForcePreset); + _statusHost.AddHandler(ActionForceMotd); + _statusHost.AddHandler(ActionPanicPunker); + + // Bandaid fix for the test fails + // "System.Collections.Generic.KeyNotFoundException : The given key 'server.admin_api_token' was not present in the dictionary." + // TODO: Figure out why this happens + try + { + _config.OnValueChanged(CCVars.AdminApiToken, UpdateToken, true); + _config.OnValueChanged(CCVars.MOTD, UpdateMotd, true); + } + catch (Exception e) + { + _sawmill.Error("Failed to subscribe to config vars: {0}", e); + } + + } + + public void Shutdown() + { + _config.UnsubValueChanged(CCVars.AdminApiToken, UpdateToken); + _config.UnsubValueChanged(CCVars.MOTD, UpdateMotd); + } + + private void UpdateToken(string token) + { + _token = token; + } + + private void UpdateMotd(string motd) + { + _motd = motd; + } + + +#region Actions + + /// + /// Changes the panic bunker settings. + /// + private async Task ActionPanicPunker(IStatusHandlerContext context) + { + if (context.RequestMethod != HttpMethod.Post || context.Url!.AbsolutePath != "/admin/actions/panic_bunker") + { + return false; + } + + if (!CheckAccess(context)) + { + await context.RespondErrorAsync(HttpStatusCode.Unauthorized); + return true; + } + + if (!CheckActor(context, out var actor)) + { + await context.RespondAsync("An actor is required to perform this action.", HttpStatusCode.BadRequest); + return true; + } + + var actionSupplied = context.RequestHeaders.TryGetValue("Action", out var action); + if (!actionSupplied) + { + await context.RespondErrorAsync(HttpStatusCode.BadRequest); + return true; + } + + var valueSupplied = context.RequestHeaders.TryGetValue("Value", out var value); + if (!valueSupplied) + { + await context.RespondErrorAsync(HttpStatusCode.BadRequest); + return true; + } + + switch (action) // TODO: This looks bad, there has to be a better way to do this. + { + case "enabled": + if (!bool.TryParse(value.ToString(), out var enabled)) + { + await context.RespondErrorAsync(HttpStatusCode.BadRequest); + return true; + } + + _taskManager.RunOnMainThread(() => + { + _config.SetCVar(CCVars.PanicBunkerEnabled, enabled); + }); + break; + case "disable_with_admins": + if (!bool.TryParse(value.ToString(), out var disableWithAdmins)) + { + await context.RespondErrorAsync(HttpStatusCode.BadRequest); + return true; + } + + _taskManager.RunOnMainThread(() => + { + _config.SetCVar(CCVars.PanicBunkerDisableWithAdmins, disableWithAdmins); + }); + break; + case "enable_without_admins": + if (!bool.TryParse(value.ToString(), out var enableWithoutAdmins)) + { + await context.RespondErrorAsync(HttpStatusCode.BadRequest); + return true; + } + + _taskManager.RunOnMainThread(() => + { + _config.SetCVar(CCVars.PanicBunkerEnableWithoutAdmins, enableWithoutAdmins); + }); + break; + case "count_deadminned_admins": + if (!bool.TryParse(value.ToString(), out var countDeadminnedAdmins)) + { + await context.RespondErrorAsync(HttpStatusCode.BadRequest); + return true; + } + + _taskManager.RunOnMainThread(() => + { + _config.SetCVar(CCVars.PanicBunkerCountDeadminnedAdmins, countDeadminnedAdmins); + }); + break; + case "show_reason": + if (!bool.TryParse(value.ToString(), out var showReason)) + { + await context.RespondErrorAsync(HttpStatusCode.BadRequest); + return true; + } + + _taskManager.RunOnMainThread(() => + { + _config.SetCVar(CCVars.PanicBunkerShowReason, showReason); + }); + break; + case "min_account_age_hours": + if (!int.TryParse(value.ToString(), out var minAccountAgeHours)) + { + await context.RespondErrorAsync(HttpStatusCode.BadRequest); + return true; + } + + _taskManager.RunOnMainThread(() => + { + _config.SetCVar(CCVars.PanicBunkerMinAccountAge, minAccountAgeHours * 60); + }); + break; + case "min_overall_hours": + if (!int.TryParse(value.ToString(), out var minOverallHours)) + { + await context.RespondErrorAsync(HttpStatusCode.BadRequest); + return true; + } + + _taskManager.RunOnMainThread(() => + { + _config.SetCVar(CCVars.PanicBunkerMinOverallHours, minOverallHours * 60); + }); + break; + } + + _sawmill.Info($"Panic bunker setting {action} changed to {value} by {actor!.Name}({actor!.Guid})."); + await context.RespondAsync("Success", HttpStatusCode.OK); + return true; + } + + /// + /// Sets the current MOTD. + /// + private async Task ActionForceMotd(IStatusHandlerContext context) + { + if (context.RequestMethod != HttpMethod.Post || context.Url!.AbsolutePath != "/admin/actions/set_motd") + { + return false; + } + + if (!CheckAccess(context)) + { + await context.RespondErrorAsync(HttpStatusCode.Unauthorized); + return true; + } + + if (!CheckActor(context, out var actor)) + { + await context.RespondAsync("An actor is required to perform this action.", HttpStatusCode.BadRequest); + return true; + } + + var motdSupplied = context.RequestHeaders.TryGetValue("MOTD", out var motd); + if (!motdSupplied) + { + await context.RespondErrorAsync(HttpStatusCode.BadRequest); + return true; + } + + _sawmill.Info($"MOTD changed to \"{motd}\" by {actor!.Name}({actor!.Guid})."); + + _taskManager.RunOnMainThread(() => _config.SetCVar(CCVars.MOTD, motd.ToString())); + // A hook in the MOTD system sends the changes to each client + await context.RespondAsync("Success", HttpStatusCode.OK); + return true; + } + + /// + /// Forces the next preset- + /// + private async Task ActionForcePreset(IStatusHandlerContext context) + { + if (context.RequestMethod != HttpMethod.Post || context.Url!.AbsolutePath != "/admin/actions/force_preset") + { + return false; + } + + if (!CheckAccess(context)) + { + await context.RespondErrorAsync(HttpStatusCode.Unauthorized); + return true; + } + + if (!CheckActor(context, out var actor)) + { + await context.RespondAsync("An actor is required to perform this action.", HttpStatusCode.BadRequest); + return true; + } + + var ticker = _entitySystemManager.GetEntitySystem(); + + if (ticker.RunLevel != GameRunLevel.PreRoundLobby) + { + _sawmill.Info($"Attempted to force preset {actor!.Name}({actor!.Guid}) while the game was not in the pre-round lobby."); + await context.RespondAsync("This can only be executed while the game is in the pre-round lobby.", HttpStatusCode.BadRequest); + return true; + } + + var presetSupplied = context.RequestHeaders.TryGetValue("PresetId", out var preset); + if (!presetSupplied) + { + await context.RespondErrorAsync(HttpStatusCode.BadRequest); + return true; + } + + var result = await RunOnMainThread(() => ticker.FindGamePreset(preset.ToString())); + if (result == null) + { + await context.RespondAsync($"No preset exists with name {preset}.", HttpStatusCode.NotFound); + return true; + } + + _taskManager.RunOnMainThread(() => + { + ticker.SetGamePreset(result); + }); + _sawmill.Info($"Forced the game to start with preset {preset} by {actor!.Name}({actor!.Guid})."); + await context.RespondAsync("Success", HttpStatusCode.OK); + return true; + } + + /// + /// Ends an active game rule. + /// + private async Task ActionEndGameRule(IStatusHandlerContext context) + { + if (context.RequestMethod != HttpMethod.Post || context.Url!.AbsolutePath != "/admin/actions/end_game_rule") + { + return false; + } + + if (!CheckAccess(context)) + { + await context.RespondErrorAsync(HttpStatusCode.Unauthorized); + return true; + } + + if (!CheckActor(context, out var actor)) + { + await context.RespondAsync("An actor is required to perform this action.", HttpStatusCode.BadRequest); + return true; + } + + var gameRuleSupplied = context.RequestHeaders.TryGetValue("GameRuleId", out var gameRule); + if (!gameRuleSupplied) + { + _sawmill.Info($"Attempted to end game rule without supplying a game rule name by {actor!.Name}({actor!.Guid})."); + await context.RespondErrorAsync(HttpStatusCode.BadRequest); + return true; + } + var ticker = _entitySystemManager.GetEntitySystem(); + var gameRuleEntity = await RunOnMainThread(() => ticker + .GetActiveGameRules() + .FirstOrNull(rule => _entityManager.MetaQuery.GetComponent(rule).EntityPrototype?.ID == gameRule.ToString())); + + if (gameRuleEntity == null) // Game rule not found + { + _sawmill.Info($"Attempted to end game rule {gameRule} by {actor!.Name}({actor!.Guid}), but it was not found."); + await context.RespondAsync("Gamerule not found or not active",HttpStatusCode.NotFound); + return true; + } + + _sawmill.Info($"Ended game rule {gameRule} by {actor!.Name}({actor!.Guid})."); + _taskManager.RunOnMainThread(() => ticker.EndGameRule((EntityUid) gameRuleEntity)); + await context.RespondAsync($"Ended game rule {gameRule}({gameRuleEntity})", HttpStatusCode.OK); + return true; + } + + /// + /// Adds a game rule to the current round. + /// + private async Task ActionAddGameRule(IStatusHandlerContext context) + { + if (context.RequestMethod != HttpMethod.Post || context.Url!.AbsolutePath != "/admin/actions/add_game_rule") + { + return false; + } + + if (!CheckAccess(context)) + { + await context.RespondErrorAsync(HttpStatusCode.Unauthorized); + return true; + } + + if (!CheckActor(context, out var actor)) + { + await context.RespondAsync("An actor is required to perform this action.", HttpStatusCode.BadRequest); + return true; + } + + var gameRuleSupplied = context.RequestHeaders.TryGetValue("GameRuleId", out var gameRule); + if (!gameRuleSupplied) + { + _sawmill.Info($"Attempted to add game rule without supplying a game rule name by {actor!.Name}({actor!.Guid})."); + await context.RespondErrorAsync(HttpStatusCode.BadRequest); + return true; + } + + var ticker = _entitySystemManager.GetEntitySystem(); + + var tsc = new TaskCompletionSource(); + _taskManager.RunOnMainThread(() => + { + var ruleEntity = ticker.AddGameRule(gameRule.ToString()); + _sawmill.Info($"Added game rule {gameRule} by {actor!.Name}({actor!.Guid})."); + if (ticker.RunLevel == GameRunLevel.InRound) + { + ticker.StartGameRule(ruleEntity); + _sawmill.Info($"Started game rule {gameRule} by {actor!.Name}({actor!.Guid})."); + } + tsc.TrySetResult(ruleEntity); + }); + + var ruleEntity = await tsc.Task; + await context.RespondAsync($"Added game rule {ruleEntity}", HttpStatusCode.OK); + return true; + } + + /// + /// Kicks a player. + /// + private async Task ActionKick(IStatusHandlerContext context) + { + if (context.RequestMethod != HttpMethod.Post || context.Url!.AbsolutePath != "/admin/actions/kick") + { + return false; + } + + if (!CheckAccess(context)) + { + await context.RespondErrorAsync(HttpStatusCode.Unauthorized); + return true; + } + + if (!CheckActor(context, out var actor)) + { + await context.RespondAsync("An actor is required to perform this action.", HttpStatusCode.BadRequest); + return true; + } + + var playerSupplied = context.RequestHeaders.TryGetValue("Guid", out var guid); + if (!playerSupplied) + { + _sawmill.Info($"Attempted to kick player without supplying a username by {actor!.Name}({actor!.Guid})."); + await context.RespondErrorAsync(HttpStatusCode.BadRequest); + return true; + } + + var session = await RunOnMainThread(() => + { + // There is no function to get a session by GUID, so we have to iterate over all sessions and check their GUIDs. + foreach (var player in _playerManager.Sessions) + { + if (player.UserId.UserId.ToString() == guid.ToString()) + { + return player; + } + } + return null; + }); + + if (session == null) + { + _sawmill.Info($"Attempted to kick player {guid} by {actor!.Name}({actor!.Guid}), but they were not found."); + await context.RespondAsync("Player not found", HttpStatusCode.NotFound); + return true; + } + + var reasonSupplied = context.RequestHeaders.TryGetValue("Reason", out var reason); + if (!reasonSupplied) + { + reason = "No reason supplied"; + } + + reason += " (kicked by admin)"; + + _taskManager.RunOnMainThread(() => + { + _netManager.DisconnectChannel(session.Channel, reason.ToString()); + }); + await context.RespondAsync("Success", HttpStatusCode.OK); + _sawmill.Info("Kicked player {0} ({1}) for {2} by {3}({4})", session.Name, session.UserId.UserId.ToString(), reason, actor!.Name, actor!.Guid); + return true; + } + + /// + /// Round restart/end + /// + private async Task ActionRoundStatus(IStatusHandlerContext context) + { + if (context.RequestMethod != HttpMethod.Post || context.Url!.AbsolutePath != "/admin/actions/round") + { + return false; + } + + if (!CheckAccess(context)) + { + await context.RespondErrorAsync(HttpStatusCode.Unauthorized); + return true; + } + + if (!CheckActor(context, out var actor)) + { + await context.RespondAsync("An actor is required to perform this action.", HttpStatusCode.BadRequest); + return true; + } + + // Not using body, because that's a stream and I don't want to deal with that + var actionSupplied = context.RequestHeaders.TryGetValue("Action", out var action); + if (!actionSupplied) + { + _sawmill.Info($"Attempted to {action} round without supplying an action by {actor!.Name}({actor!.Guid})."); + await context.RespondErrorAsync(HttpStatusCode.BadRequest); + return true; + } + + var ticker = _entitySystemManager.GetEntitySystem(); + var roundEndSystem = _entitySystemManager.GetEntitySystem(); + switch (action) + { + case "start": + if (ticker.RunLevel != GameRunLevel.PreRoundLobby) + { + await context.RespondAsync("Round already started", HttpStatusCode.BadRequest); + _sawmill.Info("Forced round start failed: round already started"); + return true; + } + _taskManager.RunOnMainThread(() => + { + ticker.StartRound(); + }); + _sawmill.Info("Forced round start"); + break; + case "end": + if (ticker.RunLevel != GameRunLevel.InRound) + { + await context.RespondAsync("Round already ended", HttpStatusCode.BadRequest); + _sawmill.Info("Forced round end failed: round is not in progress"); + return true; + } + _taskManager.RunOnMainThread(() => + { + roundEndSystem.EndRound(); + }); + _sawmill.Info("Forced round end"); + break; + case "restart": + if (ticker.RunLevel != GameRunLevel.InRound) + { + await context.RespondAsync("Round already ended", HttpStatusCode.BadRequest); + _sawmill.Info("Forced round restart failed: round is not in progress"); + return true; + } + _taskManager.RunOnMainThread(() => + { + roundEndSystem.EndRound(); + }); + _sawmill.Info("Forced round restart"); + break; + case "restartnow": // You should restart yourself NOW!!! + _taskManager.RunOnMainThread(() => + { + ticker.RestartRound(); + }); + _sawmill.Info("Forced instant round restart"); + break; + default: + await context.RespondErrorAsync(HttpStatusCode.BadRequest); + return true; + } + + _sawmill.Info($"Round {action} by {actor!.Name}({actor!.Guid})."); + await context.RespondAsync("Success", HttpStatusCode.OK); + return true; + } +#endregion + +#region Fetching + + /// + /// Returns an array containing all presets. + /// + private async Task GetForcePresets(IStatusHandlerContext context) + { + if (context.RequestMethod != HttpMethod.Get || context.Url!.AbsolutePath != "/admin/force_presets") + { + return false; + } + + if (!CheckAccess(context)) + { + await context.RespondErrorAsync(HttpStatusCode.Unauthorized); + return true; + } + + var jObject = new JsonObject(); + var presets = new List(); + foreach (var preset in _prototypeManager.EnumeratePrototypes()) + { + presets.Add(preset.ID); + } + + jObject["presets"] = JsonNode.Parse(JsonSerializer.Serialize(presets)); + await context.RespondAsync(jObject.ToString(), HttpStatusCode.OK); + return true; + } + + /// + /// Returns an array containing all game rules. + /// + private async Task GetGameRules(IStatusHandlerContext context) + { + if (context.RequestMethod != HttpMethod.Get || context.Url!.AbsolutePath != "/admin/game_rules") + { + return false; + } + + if (!CheckAccess(context)) + { + await context.RespondErrorAsync(HttpStatusCode.Unauthorized); + return true; + } + + var jObject = new JsonObject(); + var gameRules = new List(); + foreach (var gameRule in _prototypeManager.EnumeratePrototypes()) + { + if (gameRule.Abstract) + continue; + + if (gameRule.HasComponent(_componentFactory)) + gameRules.Add(gameRule.ID); + } + + jObject["game_rules"] = JsonNode.Parse(JsonSerializer.Serialize(gameRules)); + await context.RespondAsync(jObject.ToString(), HttpStatusCode.OK); + return true; + } + + + /// + /// Handles fetching information. + /// + private async Task InfoHandler(IStatusHandlerContext context) + { + if (context.RequestMethod != HttpMethod.Get || context.Url!.AbsolutePath != "/admin/info" || _token == string.Empty) + { + return false; + // 404 + } + + if (!CheckAccess(context)) + { + await context.RespondErrorAsync(HttpStatusCode.Unauthorized); + return true; + } + + if (!CheckActor(context, out var actor)) + { + await context.RespondAsync("An actor is required to perform this action.", HttpStatusCode.BadRequest); + return true; + } + + /* Information to display + Round number + Connected players + Active admins + Active game rules + Active game preset + Active map + MOTD + Panic bunker status + */ + + var ticker = _entitySystemManager.GetEntitySystem(); + var adminSystem = _entitySystemManager.GetEntitySystem(); + + var jObject = new JsonObject(); + + jObject["round_id"] = await RunOnMainThread(() => ticker.RoundId); + + var players = new List(); + var onlineAdmins = new List(); + var onlineAdminsDeadmined = new List(); + + foreach (var player in _playerManager.Sessions) + { + players.Add(new Player + { + Guid = player.UserId.UserId.ToString(), + Name = player.Name + }); + if (await RunOnMainThread(() => _adminManager.IsAdmin(player))) + { + onlineAdmins.Add(new Player + { + Guid = player.UserId.UserId.ToString(), + Name = player.Name + }); + } + else if (await RunOnMainThread(() => _adminManager.IsAdmin(player, true))) + { + onlineAdminsDeadmined.Add(new Player + { + Guid = player.UserId.UserId.ToString(), + Name = player.Name + }); + } + } + + // The JsonSerializer.Serialize into JsonNode.Parse is a bit of a hack + jObject["players"] = JsonNode.Parse(JsonSerializer.Serialize(players)); + jObject["admins"] = JsonNode.Parse(JsonSerializer.Serialize(onlineAdmins)); + jObject["deadmined"] = JsonNode.Parse(JsonSerializer.Serialize(onlineAdminsDeadmined)); + + var gameRules = new List(); + foreach (var addedGameRule in await RunOnMainThread(() => ticker.GetActiveGameRules())) + { + var meta = _entityManager.MetaQuery.GetComponent(addedGameRule); + gameRules.Add(meta.EntityPrototype?.ID ?? meta.EntityPrototype?.Name ?? "Unknown"); + } + + jObject["game_rules"] = JsonNode.Parse(JsonSerializer.Serialize(gameRules)); + jObject["game_preset"] = ticker.CurrentPreset?.ID; + jObject["map"] = await RunOnMainThread(() => _gameMapManager.GetSelectedMap()?.MapName ?? "Unknown"); + jObject["motd"] = _motd; + jObject["panic_bunker"] = new JsonObject(); + jObject["panic_bunker"]!["enabled"] = adminSystem.PanicBunker.Enabled; + jObject["panic_bunker"]!["disable_with_admins"] = adminSystem.PanicBunker.DisableWithAdmins; + jObject["panic_bunker"]!["enable_without_admins"] = adminSystem.PanicBunker.EnableWithoutAdmins; + jObject["panic_bunker"]!["count_deadminned_admins"] = adminSystem.PanicBunker.CountDeadminnedAdmins; + jObject["panic_bunker"]!["show_reason"] = adminSystem.PanicBunker.ShowReason; + jObject["panic_bunker"]!["min_account_age_hours"] = adminSystem.PanicBunker.MinAccountAgeHours; + jObject["panic_bunker"]!["min_overall_hours"] = adminSystem.PanicBunker.MinOverallHours; + + _sawmill.Info($"Info requested by {actor!.Name}({actor!.Guid})."); + await context.RespondAsync(jObject.ToString(), HttpStatusCode.OK); + return true; + } + +#endregion + + private bool CheckAccess(IStatusHandlerContext context) + { + var auth = context.RequestHeaders.TryGetValue("Authorization", out var authToken); + if (!auth) + { + _sawmill.Info(@"Unauthorized access attempt to admin API. No auth header"); + return false; + } // No auth header, no access + + if (authToken == _token) + return true; + + // Invalid auth header, no access + _sawmill.Info(@"Unauthorized access attempt to admin API. ""{0}""", authToken.ToString()); + return false; + } + + /// + /// Async helper function which runs a task on the main thread and returns the result. + /// + private async Task RunOnMainThread(Func func) + { + var taskCompletionSource = new TaskCompletionSource(); + _taskManager.RunOnMainThread(() => + { + taskCompletionSource.TrySetResult(func()); + }); + + var result = await taskCompletionSource.Task; + return result; + } + + private bool CheckActor(IStatusHandlerContext context, out Player? actor) + { + // We are trusting the header to be correct. + // This is fine because the header is set by the SS14.Admin backend. + var actorSupplied = context.RequestHeaders.TryGetValue("Actor", out var actorHeader); + if (!actorSupplied) + { + actor = null; + return false; + } + + var stringRep = actorHeader.ToString(); + actor = new Player() + { + // GUID_NAME format + Guid = stringRep[..stringRep.IndexOf('_')], + Name = stringRep[(stringRep.IndexOf('_') + 1)..] + }; + return true; + } + + private sealed class Player + { + public string Guid { get; set; } = default!; + public string Name { get; set; } = default!; + } +} diff --git a/Content.Server/Administration/Systems/AdminSystem.cs b/Content.Server/Administration/Systems/AdminSystem.cs index 966bff2f71..70d3befbe2 100644 --- a/Content.Server/Administration/Systems/AdminSystem.cs +++ b/Content.Server/Administration/Systems/AdminSystem.cs @@ -61,7 +61,7 @@ namespace Content.Server.Administration.Systems public IReadOnlySet RoundActivePlayers => _roundActivePlayers; private readonly HashSet _roundActivePlayers = new(); - private readonly PanicBunkerStatus _panicBunker = new(); + public readonly PanicBunkerStatus PanicBunker = new(); public override void Initialize() { @@ -248,7 +248,7 @@ namespace Content.Server.Administration.Systems private void OnPanicBunkerChanged(bool enabled) { - _panicBunker.Enabled = enabled; + PanicBunker.Enabled = enabled; _chat.SendAdminAlert(Loc.GetString(enabled ? "admin-ui-panic-bunker-enabled-admin-alert" : "admin-ui-panic-bunker-disabled-admin-alert" @@ -259,52 +259,52 @@ namespace Content.Server.Administration.Systems private void OnPanicBunkerDisableWithAdminsChanged(bool enabled) { - _panicBunker.DisableWithAdmins = enabled; + PanicBunker.DisableWithAdmins = enabled; UpdatePanicBunker(); } private void OnPanicBunkerEnableWithoutAdminsChanged(bool enabled) { - _panicBunker.EnableWithoutAdmins = enabled; + PanicBunker.EnableWithoutAdmins = enabled; UpdatePanicBunker(); } private void OnPanicBunkerCountDeadminnedAdminsChanged(bool enabled) { - _panicBunker.CountDeadminnedAdmins = enabled; + PanicBunker.CountDeadminnedAdmins = enabled; UpdatePanicBunker(); } private void OnShowReasonChanged(bool enabled) { - _panicBunker.ShowReason = enabled; + PanicBunker.ShowReason = enabled; SendPanicBunkerStatusAll(); } private void OnPanicBunkerMinAccountAgeChanged(int minutes) { - _panicBunker.MinAccountAgeHours = minutes / 60; + PanicBunker.MinAccountAgeHours = minutes / 60; SendPanicBunkerStatusAll(); } private void OnPanicBunkerMinOverallHoursChanged(int hours) { - _panicBunker.MinOverallHours = hours; + PanicBunker.MinOverallHours = hours; SendPanicBunkerStatusAll(); } private void UpdatePanicBunker() { - var admins = _panicBunker.CountDeadminnedAdmins + var admins = PanicBunker.CountDeadminnedAdmins ? _adminManager.AllAdmins : _adminManager.ActiveAdmins; var hasAdmins = admins.Any(); - if (hasAdmins && _panicBunker.DisableWithAdmins) + if (hasAdmins && PanicBunker.DisableWithAdmins) { _config.SetCVar(CCVars.PanicBunkerEnabled, false); } - else if (!hasAdmins && _panicBunker.EnableWithoutAdmins) + else if (!hasAdmins && PanicBunker.EnableWithoutAdmins) { _config.SetCVar(CCVars.PanicBunkerEnabled, true); } @@ -314,7 +314,7 @@ namespace Content.Server.Administration.Systems private void SendPanicBunkerStatusAll() { - var ev = new PanicBunkerChangedEvent(_panicBunker); + var ev = new PanicBunkerChangedEvent(PanicBunker); foreach (var admin in _adminManager.AllAdmins) { RaiseNetworkEvent(ev, admin); diff --git a/Content.Server/IoC/ServerContentIoC.cs b/Content.Server/IoC/ServerContentIoC.cs index 2a63ace8e3..25bb1072a5 100644 --- a/Content.Server/IoC/ServerContentIoC.cs +++ b/Content.Server/IoC/ServerContentIoC.cs @@ -58,6 +58,7 @@ namespace Content.Server.IoC IoCManager.Register(); IoCManager.Register(); IoCManager.Register(); + IoCManager.Register(); } } } diff --git a/Content.Shared/CCVar/CCVars.cs b/Content.Shared/CCVar/CCVars.cs index 5a315f7055..2a1a29c044 100644 --- a/Content.Shared/CCVar/CCVars.cs +++ b/Content.Shared/CCVar/CCVars.cs @@ -29,6 +29,12 @@ namespace Content.Shared.CCVar public static readonly CVarDef RulesHeader = CVarDef.Create("server.rules_header", "ui-rules-header", CVar.REPLICATED | CVar.SERVER); + /// + /// The token used to authenticate with the admin API. Leave empty to disable the admin API. This is a secret! Do not share! + /// + public static readonly CVarDef AdminApiToken = + CVarDef.Create("server.admin_api_token", string.Empty, CVar.SERVERONLY | CVar.CONFIDENTIAL); + /* * Ambience */