Files
crystall-punk-14/Content.Server/Administration/ServerApi.cs
Ed 684ec72541 Upstrearm sync (#94)
* Fix for the salvage ice labs map. (#26928)

* done

* more work

* Automatic changelog update

* Update Credits (#26938)

Co-authored-by: PJBot <pieterjan.briers+bot@gmail.com>

* Fix cryostorage identifying unknown characters as captain (#26927)

Fixed cryostorage getting captain's record for unknown jobs.
Also localized Unknown job string.

* Automatic changelog update

* Fixed Honkbot/jonkbot honking like crazy, gave honkbot/jonkbot standard idle ai. (#26939)

* Fixed Honkbot/jonkbot honking like crazy, gave honkbot/jonkbot standard idle ai.

* Update Resources/Prototypes/Entities/Mobs/NPCs/silicon.yml

---------

Co-authored-by: Tayrtahn <tayrtahn@gmail.com>

* Automatic changelog update

* Bug fix: Force cancellation of RCD constructions if the construction type is changed (#26935)

Force cancellation of RCD constructions if the construction type is changed

* Fix standart -> standard and dressfilled test fail (#26942)

Fix standart -> standard

* Add Ability to stop sound when MobState is Dead (#26905)

* Add stopsWhenEntityDead to sound components

* Convert component

* Review

* Fix dupe sub

---------

Co-authored-by: metalgearsloth <comedian_vs_clown@hotmail.com>

* Automatic changelog update

* Fix rockets and lasers looking like they have nothing loaded (#26933)

* Automatic changelog update

* You can now see paper on crates (with color!) (#26834)

* Implement changes on not-cooked branch

* Made it work

* Fix update appearance calls

* Fix extra indents, clean-up code, fix tests hopefully

* Fix hammy cagecrate

* Fix messing up the yml, add artifact crate specific labels back in

* Visual Studio hates yml, sad

* Seperate the colors for cargonia

* sorry json

* make label move with artifact door

* Apply suggestion changes

Co-authored-by: Nemanja <98561806+EmoGarbage404@users.noreply.github.com>

* Fix remaining crate offsets, add a few for livestock and graves (why are you labeling graves) and coffin label sprites (why are you labeling coffins??)

---------

Co-authored-by: Nemanja <98561806+EmoGarbage404@users.noreply.github.com>

* Make UtensilSystem and SharpSystem not run AfterInteract if it has already been handled (#25826)

* Make UtensilSystem and SharpSystem not run AfterInteract if it has already been handled

* merge conflicts

---------

Co-authored-by: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com>
Co-authored-by: metalgearsloth <comedian_vs_clown@hotmail.com>

* Automatic changelog update

* Add two-message overload to PopupPredicted (#26907)

Added two-message overload to PopupPredicted

* Update submodule to 218.0.0 (#26945)

* Autism pins! (#25597)

* hee hee he ha ha

* added gold varients, forgive me for my spritework

* maints loot, copying from past PRs

* Trying to fix RSI

* speedran these sprites in break time, pictures will be later

* Fixed/Tweaked glows

* consensus

* gregregation

* dam copiryte

* oops i forgot to delete 2 fields hope this works

* Automatic changelog update

* Fix database round start date issues (#26838)

How can ONE DATABASE COLUMN have so many cursed issues I don't know, but it certainly pissed off the devil in its previous life.

The start_date column on round entities in the database was added by https://github.com/space-wizards/space-station-14/pull/21153. For some reason, this PR gave the column a nonsensical default value instead of making it nullable. This default value causes the code from #25280 to break. It actually trips an assert though that's not what the original issue report ran into.

This didn't get noticed on wizden servers because we at some point backfilled the start_date column based on the stored admin logs.

So I change the database model to make this column nullable, updated the C# code to match, and made the existing migration set the invalid values to be NULL instead. Cool.

Wait how's SQLite handle in this scenario anyways? Well actually turns out the column was *completely broken* in the first place!

The code for inserting into the round table was copy pasted between SQLite and PostgreSQL, with the only difference being that the SQLite key manually assigned the primary key instead of letting SQLite AUTOINCREMENT it. And then the code to give a start_date value was only added to the PostgreSQL version (which is actually in the base class already). So for SQLite that column's been filled up with the same invalid default the whole time.

Why was the code manually assigning a PK? I checked the SQLite docs for AUTOINCREMENT[1], and the behavior seems appropriate.

I removed the SQLite-specific code path and it just seems to work regardless. The migration just sets the old values to NULL too.

BUT WAIT, THERE'S MORE!

Turns out just doing the migration on SQLite is a pain in the ass! EF Core has to create a new table to apply the nullability change, because SQLite doesn't support proper ALTER COLUMN. This causes the generated SQL commands to be weird and the UPDATE for the migration goes BEFORE the nullability change... I ended up having to make TWO migrations for SQLite. Yay.

Fixes #26800

[1]: https://www.sqlite.org/autoinc.html

* Fix options menu crashing in replays (#26911)

Not having the nullable set properly is annoying but fixing that would probably be a significant amount of work.

* Greyscale color clothing (#26943)

* greyscales color gloves, color jumpsuits, and shoes

* remove popbob

* fix test fails

* Automatic changelog update

* WT550 Buffs + Burst Mode for WT550 & C-20R (#26886)

* Slightly increased WT550 Firerate, drastically reduced recoil, and given it the option to fire in 5 round bursts.

* Given the C-20 a 5 round burst aswell

* Automatic changelog update

* make holoparasites actually holographic (#26862)

it's over

* Automatic changelog update

* Add character sheets to board game crate (#26926)

add character sheets to board game crate

* Automatic changelog update

* Game server admin API (#26880)

* Reapply "Game server api" (#26871)

This reverts commit 3aee197923.

* Rewrite 75% of the code it's good now

* Wield recoil components (#26915)

* WieldRecoilComponents

* WieldRecoilComponents

* Update Content.Shared/Weapons/Ranged/Components/GunWieldBonusComponent.cs

Co-authored-by: Whisper <121047731+QuietlyWhisper@users.noreply.github.com>

* Update Content.Shared/Weapons/Ranged/Components/GunWieldBonusComponent.cs

---------

Co-authored-by: Whisper <121047731+QuietlyWhisper@users.noreply.github.com>
Co-authored-by: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com>

* Clown shoes make you waddle, as God intended (#26338)

* Clown shoes make you waddle, as God intended

* OOPS

* Toned down, client system name fix

* Tidy namespacing for @deltanedas

* Refactor to handle prediction better, etc.

* Resolve PR comments.

* Automatic changelog update

* Use round time instead of server time for criminal history (#26949)

make criminal records computer use round time for history instead of the server time

* Rotate and Offset station CCVar nuke (#26175)

* no content

* add noRot to Europa

* bruh. and this

* yay

* fix

* Update debug.yml

---------

Co-authored-by: Boaz1111 <149967078+Boaz1111@users.noreply.github.com>
Co-authored-by: PJBot <pieterjan.briers+bot@gmail.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Tayrtahn <tayrtahn@gmail.com>
Co-authored-by: superjj18 <gagnonjake@gmail.com>
Co-authored-by: chromiumboy <50505512+chromiumboy@users.noreply.github.com>
Co-authored-by: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com>
Co-authored-by: GreaseMonk <1354802+GreaseMonk@users.noreply.github.com>
Co-authored-by: metalgearsloth <comedian_vs_clown@hotmail.com>
Co-authored-by: DrSmugleaf <10968691+DrSmugleaf@users.noreply.github.com>
Co-authored-by: Verm <32827189+Vermidia@users.noreply.github.com>
Co-authored-by: Nemanja <98561806+EmoGarbage404@users.noreply.github.com>
Co-authored-by: Terraspark4941 <terraspark4941@gmail.com>
Co-authored-by: Pieter-Jan Briers <pieterjan.briers+git@gmail.com>
Co-authored-by: Flareguy <78941145+Flareguy@users.noreply.github.com>
Co-authored-by: BramvanZijp <56019239+BramvanZijp@users.noreply.github.com>
Co-authored-by: Mr. 27 <45323883+Dutch-VanDerLinde@users.noreply.github.com>
Co-authored-by: Tyzemol <85772526+Tyzemol@users.noreply.github.com>
Co-authored-by: Froffy025 <78222136+Froffy025@users.noreply.github.com>
Co-authored-by: Whisper <121047731+QuietlyWhisper@users.noreply.github.com>
Co-authored-by: Hannah Giovanna Dawson <karakkaraz@gmail.com>
Co-authored-by: ilya.mikheev.coder <imc-ext+github@ilyamikcoder.com>
2024-04-14 20:34:16 +03:00

712 lines
22 KiB
C#

using System.Linq;
using System.Net;
using System.Net.Http;
using System.Security.Cryptography;
using System.Text;
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;
/// <summary>
/// Exposes various admin-related APIs via the game server's <see cref="StatusHost"/>.
/// </summary>
public sealed partial class ServerApi : IPostInjectInit
{
private const string SS14TokenScheme = "SS14Token";
private static readonly HashSet<string> PanicBunkerCVars =
[
CCVars.PanicBunkerEnabled.Name,
CCVars.PanicBunkerDisableWithAdmins.Name,
CCVars.PanicBunkerEnableWithoutAdmins.Name,
CCVars.PanicBunkerCountDeadminnedAdmins.Name,
CCVars.PanicBunkerShowReason.Name,
CCVars.PanicBunkerMinAccountAge.Name,
CCVars.PanicBunkerMinOverallHours.Name,
CCVars.PanicBunkerCustomReason.Name,
];
[Dependency] private readonly IStatusHost _statusHost = default!;
[Dependency] private readonly IConfigurationManager _config = default!;
[Dependency] private readonly ISharedPlayerManager _playerManager = default!;
[Dependency] private readonly ISharedAdminManager _adminManager = default!;
[Dependency] private readonly IGameMapManager _gameMapManager = default!;
[Dependency] private readonly IServerNetManager _netManager = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly IComponentFactory _componentFactory = default!;
[Dependency] private readonly ITaskManager _taskManager = default!;
[Dependency] private readonly EntityManager _entityManager = default!;
[Dependency] private readonly ILogManager _logManager = default!;
[Dependency] private readonly IEntitySystemManager _entitySystemManager = default!;
[Dependency] private readonly ILocalizationManager _loc = default!;
private string _token = string.Empty;
private ISawmill _sawmill = default!;
void IPostInjectInit.PostInject()
{
_sawmill = _logManager.GetSawmill("serverApi");
// Get
RegisterActorHandler(HttpMethod.Get, "/admin/info", InfoHandler);
RegisterHandler(HttpMethod.Get, "/admin/game_rules", GetGameRules);
RegisterHandler(HttpMethod.Get, "/admin/presets", GetPresets);
// Post
RegisterActorHandler(HttpMethod.Post, "/admin/actions/round/start", ActionRoundStart);
RegisterActorHandler(HttpMethod.Post, "/admin/actions/round/end", ActionRoundEnd);
RegisterActorHandler(HttpMethod.Post, "/admin/actions/round/restartnow", ActionRoundRestartNow);
RegisterActorHandler(HttpMethod.Post, "/admin/actions/kick", ActionKick);
RegisterActorHandler(HttpMethod.Post, "/admin/actions/add_game_rule", ActionAddGameRule);
RegisterActorHandler(HttpMethod.Post, "/admin/actions/end_game_rule", ActionEndGameRule);
RegisterActorHandler(HttpMethod.Post, "/admin/actions/force_preset", ActionForcePreset);
RegisterActorHandler(HttpMethod.Post, "/admin/actions/set_motd", ActionForceMotd);
RegisterActorHandler(HttpMethod.Patch, "/admin/actions/panic_bunker", ActionPanicPunker);
}
public void Initialize()
{
_config.OnValueChanged(CCVars.AdminApiToken, UpdateToken, true);
}
public void Shutdown()
{
_config.UnsubValueChanged(CCVars.AdminApiToken, UpdateToken);
}
private void UpdateToken(string token)
{
_token = token;
}
#region Actions
/// <summary>
/// Changes the panic bunker settings.
/// </summary>
private async Task ActionPanicPunker(IStatusHandlerContext context, Actor actor)
{
var request = await ReadJson<JsonObject>(context);
if (request == null)
return;
var toSet = new Dictionary<string, object>();
foreach (var (cVar, value) in request)
{
if (!PanicBunkerCVars.Contains(cVar))
{
await RespondBadRequest(context, $"Invalid panic bunker CVar: '{cVar}'");
return;
}
if (value == null)
{
await RespondBadRequest(context, $"Value is null: '{cVar}'");
return;
}
if (value is not JsonValue jsonValue)
{
await RespondBadRequest(context, $"Value is not valid: '{cVar}'");
return;
}
object castValue;
var cVarType = _config.GetCVarType(cVar);
if (cVarType == typeof(bool))
{
if (!jsonValue.TryGetValue(out bool b))
{
await RespondBadRequest(context, $"CVar '{cVar}' must be of type bool.");
return;
}
castValue = b;
}
else if (cVarType == typeof(int))
{
if (!jsonValue.TryGetValue(out int i))
{
await RespondBadRequest(context, $"CVar '{cVar}' must be of type int.");
return;
}
castValue = i;
}
else if (cVarType == typeof(string))
{
if (!jsonValue.TryGetValue(out string? s))
{
await RespondBadRequest(context, $"CVar '{cVar}' must be of type string.");
return;
}
castValue = s;
}
else
{
throw new NotSupportedException("Unsupported CVar type");
}
toSet[cVar] = castValue;
}
await RunOnMainThread(() =>
{
foreach (var (cVar, value) in toSet)
{
_config.SetCVar(cVar, value);
_sawmill.Info(
$"Panic bunker property '{cVar}' changed to '{value}' by {FormatLogActor(actor)}.");
}
});
await RespondOk(context);
}
/// <summary>
/// Sets the current MOTD.
/// </summary>
private async Task ActionForceMotd(IStatusHandlerContext context, Actor actor)
{
var motd = await ReadJson<MotdActionBody>(context);
if (motd == null)
return;
_sawmill.Info($"MOTD changed to \"{motd.Motd}\" by {FormatLogActor(actor)}.");
await RunOnMainThread(() => _config.SetCVar(CCVars.MOTD, motd.Motd));
// A hook in the MOTD system sends the changes to each client
await RespondOk(context);
}
/// <summary>
/// Forces the next preset-
/// </summary>
private async Task ActionForcePreset(IStatusHandlerContext context, Actor actor)
{
var body = await ReadJson<PresetActionBody>(context);
if (body == null)
return;
await RunOnMainThread(async () =>
{
var ticker = _entitySystemManager.GetEntitySystem<GameTicker>();
if (ticker.RunLevel != GameRunLevel.PreRoundLobby)
{
await RespondError(
context,
ErrorCode.InvalidRoundState,
HttpStatusCode.Conflict,
"Game must be in pre-round lobby");
return;
}
var preset = ticker.FindGamePreset(body.PresetId);
if (preset == null)
{
await RespondError(
context,
ErrorCode.GameRuleNotFound,
HttpStatusCode.UnprocessableContent,
$"Game rule '{body.PresetId}' doesn't exist");
return;
}
ticker.SetGamePreset(preset);
_sawmill.Info($"Forced the game to start with preset {body.PresetId} by {FormatLogActor(actor)}.");
await RespondOk(context);
});
}
/// <summary>
/// Ends an active game rule.
/// </summary>
private async Task ActionEndGameRule(IStatusHandlerContext context, Actor actor)
{
var body = await ReadJson<GameRuleActionBody>(context);
if (body == null)
return;
await RunOnMainThread(async () =>
{
var ticker = _entitySystemManager.GetEntitySystem<GameTicker>();
var gameRule = ticker
.GetActiveGameRules()
.FirstOrNull(rule =>
_entityManager.MetaQuery.GetComponent(rule).EntityPrototype?.ID == body.GameRuleId);
if (gameRule == null)
{
await RespondError(context,
ErrorCode.GameRuleNotFound,
HttpStatusCode.UnprocessableContent,
$"Game rule '{body.GameRuleId}' not found or not active");
return;
}
_sawmill.Info($"Ended game rule {body.GameRuleId} by {FormatLogActor(actor)}.");
ticker.EndGameRule(gameRule.Value);
await RespondOk(context);
});
}
/// <summary>
/// Adds a game rule to the current round.
/// </summary>
private async Task ActionAddGameRule(IStatusHandlerContext context, Actor actor)
{
var body = await ReadJson<GameRuleActionBody>(context);
if (body == null)
return;
await RunOnMainThread(async () =>
{
var ticker = _entitySystemManager.GetEntitySystem<GameTicker>();
if (!_prototypeManager.HasIndex<EntityPrototype>(body.GameRuleId))
{
await RespondError(context,
ErrorCode.GameRuleNotFound,
HttpStatusCode.UnprocessableContent,
$"Game rule '{body.GameRuleId}' not found or not active");
return;
}
var ruleEntity = ticker.AddGameRule(body.GameRuleId);
_sawmill.Info($"Added game rule {body.GameRuleId} by {FormatLogActor(actor)}.");
if (ticker.RunLevel == GameRunLevel.InRound)
{
ticker.StartGameRule(ruleEntity);
_sawmill.Info($"Started game rule {body.GameRuleId} by {FormatLogActor(actor)}.");
}
await RespondOk(context);
});
}
/// <summary>
/// Kicks a player.
/// </summary>
private async Task ActionKick(IStatusHandlerContext context, Actor actor)
{
var body = await ReadJson<KickActionBody>(context);
if (body == null)
return;
await RunOnMainThread(async () =>
{
if (!_playerManager.TryGetSessionById(new NetUserId(body.Guid), out var player))
{
await RespondError(
context,
ErrorCode.PlayerNotFound,
HttpStatusCode.UnprocessableContent,
"Player not found");
return;
}
var reason = body.Reason ?? "No reason supplied";
reason += " (kicked by admin)";
_netManager.DisconnectChannel(player.Channel, reason);
await RespondOk(context);
_sawmill.Info($"Kicked player {player.Name} ({player.UserId}) for {reason} by {FormatLogActor(actor)}");
});
}
private async Task ActionRoundStart(IStatusHandlerContext context, Actor actor)
{
await RunOnMainThread(async () =>
{
var ticker = _entitySystemManager.GetEntitySystem<GameTicker>();
if (ticker.RunLevel != GameRunLevel.PreRoundLobby)
{
await RespondError(
context,
ErrorCode.InvalidRoundState,
HttpStatusCode.Conflict,
"Round already started");
return;
}
ticker.StartRound();
_sawmill.Info($"Forced round start by {FormatLogActor(actor)}");
await RespondOk(context);
});
}
private async Task ActionRoundEnd(IStatusHandlerContext context, Actor actor)
{
await RunOnMainThread(async () =>
{
var roundEndSystem = _entitySystemManager.GetEntitySystem<RoundEndSystem>();
var ticker = _entitySystemManager.GetEntitySystem<GameTicker>();
if (ticker.RunLevel != GameRunLevel.InRound)
{
await RespondError(
context,
ErrorCode.InvalidRoundState,
HttpStatusCode.Conflict,
"Round is not active");
return;
}
roundEndSystem.EndRound();
_sawmill.Info($"Forced round end by {FormatLogActor(actor)}");
await RespondOk(context);
});
}
private async Task ActionRoundRestartNow(IStatusHandlerContext context, Actor actor)
{
await RunOnMainThread(async () =>
{
var ticker = _entitySystemManager.GetEntitySystem<GameTicker>();
ticker.RestartRound();
_sawmill.Info($"Forced instant round restart by {FormatLogActor(actor)}");
await RespondOk(context);
});
}
#endregion
#region Fetching
/// <summary>
/// Returns an array containing all available presets.
/// </summary>
private async Task GetPresets(IStatusHandlerContext context)
{
var presets = await RunOnMainThread(() =>
{
var presets = new List<PresetResponse.Preset>();
foreach (var preset in _prototypeManager.EnumeratePrototypes<GamePresetPrototype>())
{
presets.Add(new PresetResponse.Preset
{
Id = preset.ID,
ModeTitle = _loc.GetString(preset.ModeTitle),
Description = _loc.GetString(preset.Description)
});
}
return presets;
});
await context.RespondJsonAsync(new PresetResponse
{
Presets = presets
});
}
/// <summary>
/// Returns an array containing all game rules.
/// </summary>
private async Task GetGameRules(IStatusHandlerContext context)
{
var gameRules = new List<string>();
foreach (var gameRule in _prototypeManager.EnumeratePrototypes<EntityPrototype>())
{
if (gameRule.Abstract)
continue;
if (gameRule.HasComponent<GameRuleComponent>(_componentFactory))
gameRules.Add(gameRule.ID);
}
await context.RespondJsonAsync(new GameruleResponse
{
GameRules = gameRules
});
}
/// <summary>
/// Handles fetching information.
/// </summary>
private async Task InfoHandler(IStatusHandlerContext context, Actor actor)
{
/*
Information to display
Round number
Connected players
Active admins
Active game rules
Active game preset
Active map
MOTD
Panic bunker status
*/
var info = await RunOnMainThread<InfoResponse>(() =>
{
var ticker = _entitySystemManager.GetEntitySystem<GameTicker>();
var adminSystem = _entitySystemManager.GetEntitySystem<AdminSystem>();
var players = new List<InfoResponse.Player>();
foreach (var player in _playerManager.Sessions)
{
var adminData = _adminManager.GetAdminData(player, true);
players.Add(new InfoResponse.Player
{
UserId = player.UserId.UserId,
Name = player.Name,
IsAdmin = adminData != null,
IsDeadminned = !adminData?.Active ?? false
});
}
InfoResponse.MapInfo? mapInfo = null;
if (_gameMapManager.GetSelectedMap() is { } mapPrototype)
{
mapInfo = new InfoResponse.MapInfo
{
Id = mapPrototype.ID,
Name = mapPrototype.MapName
};
}
var gameRules = new List<string>();
foreach (var addedGameRule in ticker.GetActiveGameRules())
{
var meta = _entityManager.MetaQuery.GetComponent(addedGameRule);
gameRules.Add(meta.EntityPrototype?.ID ?? meta.EntityPrototype?.Name ?? "Unknown");
}
var panicBunkerCVars = PanicBunkerCVars.ToDictionary(c => c, c => _config.GetCVar(c));
return new InfoResponse
{
Players = players,
RoundId = ticker.RoundId,
Map = mapInfo,
PanicBunker = panicBunkerCVars,
GamePreset = ticker.CurrentPreset?.ID,
GameRules = gameRules,
MOTD = _config.GetCVar(CCVars.MOTD)
};
});
await context.RespondJsonAsync(info);
}
#endregion
private async Task<bool> CheckAccess(IStatusHandlerContext context)
{
var auth = context.RequestHeaders.TryGetValue("Authorization", out var authToken);
if (!auth)
{
await RespondError(
context,
ErrorCode.AuthenticationNeeded,
HttpStatusCode.Unauthorized,
"Authorization is required");
return false;
}
var authHeaderValue = authToken.ToString();
var spaceIndex = authHeaderValue.IndexOf(' ');
if (spaceIndex == -1)
{
await RespondBadRequest(context, "Invalid Authorization header value");
return false;
}
var authScheme = authHeaderValue[..spaceIndex];
var authValue = authHeaderValue[spaceIndex..].Trim();
if (authScheme != SS14TokenScheme)
{
await RespondBadRequest(context, "Invalid Authorization scheme");
return false;
}
if (_token == "")
{
_sawmill.Debug("No authorization token set for admin API");
}
else if (CryptographicOperations.FixedTimeEquals(
Encoding.UTF8.GetBytes(authValue),
Encoding.UTF8.GetBytes(_token)))
{
return true;
}
await RespondError(
context,
ErrorCode.AuthenticationInvalid,
HttpStatusCode.Unauthorized,
"Authorization is invalid");
// Invalid auth header, no access
_sawmill.Info($"Unauthorized access attempt to admin API from {context.RemoteEndPoint}");
return false;
}
private async Task<Actor?> CheckActor(IStatusHandlerContext context)
{
// The actor is JSON encoded in the header
var actor = context.RequestHeaders.TryGetValue("Actor", out var actorHeader) ? actorHeader.ToString() : null;
if (actor == null)
{
await RespondBadRequest(context, "Actor must be supplied");
return null;
}
Actor? actorData;
try
{
actorData = JsonSerializer.Deserialize<Actor>(actor);
if (actorData == null)
{
await RespondBadRequest(context, "Actor is null");
return null;
}
}
catch (JsonException exception)
{
await RespondBadRequest(context, "Actor field JSON is invalid", ExceptionData.FromException(exception));
return null;
}
return actorData;
}
#region From Client
private sealed class Actor
{
public required Guid Guid { get; init; }
public required string Name { get; init; }
}
private sealed class KickActionBody
{
public required Guid Guid { get; init; }
public string? Reason { get; init; }
}
private sealed class GameRuleActionBody
{
public required string GameRuleId { get; init; }
}
private sealed class PresetActionBody
{
public required string PresetId { get; init; }
}
private sealed class MotdActionBody
{
public required string Motd { get; init; }
}
#endregion
#region Responses
private record BaseResponse(
string Message,
ErrorCode ErrorCode = ErrorCode.None,
ExceptionData? Exception = null);
private record ExceptionData(string Message, string? StackTrace = null)
{
public static ExceptionData FromException(Exception e)
{
return new ExceptionData(e.Message, e.StackTrace);
}
}
private enum ErrorCode
{
None = 0,
AuthenticationNeeded = 1,
AuthenticationInvalid = 2,
InvalidRoundState = 3,
PlayerNotFound = 4,
GameRuleNotFound = 5,
BadRequest = 6,
}
#endregion
#region Misc
/// <summary>
/// Record used to send the response for the info endpoint.
/// </summary>
private sealed class InfoResponse
{
public required int RoundId { get; init; }
public required List<Player> Players { get; init; }
public required List<string> GameRules { get; init; }
public required string? GamePreset { get; init; }
public required MapInfo? Map { get; init; }
public required string? MOTD { get; init; }
public required Dictionary<string, object> PanicBunker { get; init; }
public sealed class Player
{
public required Guid UserId { get; init; }
public required string Name { get; init; }
public required bool IsAdmin { get; init; }
public required bool IsDeadminned { get; init; }
}
public sealed class MapInfo
{
public required string Id { get; init; }
public required string Name { get; init; }
}
}
private sealed class PresetResponse
{
public required List<Preset> Presets { get; init; }
public sealed class Preset
{
public required string Id { get; init; }
public required string Description { get; init; }
public required string ModeTitle { get; init; }
}
}
private sealed class GameruleResponse
{
public required List<string> GameRules { get; init; }
}
#endregion
}