From ad58a056d780585dd30e15228567697af615d733 Mon Sep 17 00:00:00 2001 From: Pieter-Jan Briers Date: Fri, 30 Oct 2020 16:06:48 +0100 Subject: [PATCH 01/11] ConGroups are gone. Long live admin flags in content. --- .../Administration/ClientAdminManager.cs | 82 +++ .../Administration/IClientAdminManager.cs | 17 + Content.Client/ClientContentIoC.cs | 4 +- Content.Client/EntryPoint.cs | 2 + .../ContentIntegrationTest.cs | 2 - .../20201028210620_Admins.Designer.cs | 509 ++++++++++++++++++ .../Postgres/20201028210620_Admins.cs | 115 ++++ .../PostgresServerDbContextModelSnapshot.cs | 118 ++++ .../Sqlite/20201028210616_Admins.Designer.cs | 476 ++++++++++++++++ .../Sqlite/20201028210616_Admins.cs | 114 ++++ .../SqliteServerDbContextModelSnapshot.cs | 115 ++++ Content.Server.Database/Model.cs | 50 ++ .../Administration/AdminCommandAttribute.cs | 27 + Content.Server/Administration/AdminManager.cs | 377 +++++++++++++ Content.Server/Administration/AdminRank.cs | 18 + .../Administration/AnyCommandAttribute.cs | 18 + .../Administration/{ => Commands}/AGhost.cs | 4 +- .../{ => Commands}/BanCommand.cs | 5 +- .../{ => Commands}/ControlMob.cs | 4 +- .../Administration/Commands/DeAdminCommand.cs | 31 ++ .../DeleteEntitiesWithComponent.cs | 4 +- .../{ => Commands}/DeleteEntitiesWithId.cs | 4 +- .../Administration/Commands/ReAdminCommand.cs | 35 ++ .../Administration/{ => Commands}/ReadyAll.cs | 4 +- .../{ => Commands}/Rejuvenate.cs | 4 +- .../{ => Commands}/WarpCommand.cs | 4 +- .../Administration/IAdminManager.cs | 47 ++ Content.Server/Atmos/AtmosCommands.cs | 3 + Content.Server/Chat/ChatCommands.cs | 7 + Content.Server/Chat/ChatManager.cs | 12 +- .../Commands/AttachBodyPartCommand.cs | 3 + .../Commands/HideContainedContextCommand.cs | 3 + .../Commands/ShowContainedContextCommand.cs | 3 + Content.Server/Database/ServerDbBase.cs | 9 + Content.Server/Database/ServerDbManager.cs | 8 + Content.Server/EntryPoint.cs | 4 +- .../Components/Body/BodyCommands.cs | 6 + .../Components/Damage/DamageCommands.cs | 5 + .../Components/Disposal/DisposalCommands.cs | 3 + .../Components/Interactable/ToolCommands.cs | 8 +- .../Components/Mobs/Speech/SpeechComponent.cs | 9 +- .../Components/Observer/GhostComponent.cs | 4 +- .../EntitySystems/AI/AiFactionTagSystem.cs | 29 +- .../GameObjects/EntitySystems/AI/AiSystem.cs | 7 +- .../EntitySystems/SignalLinkerSystem.cs | 3 + Content.Server/GameTicking/GameTicker.cs | 1 + .../GameTicking/GameTickerCommands.cs | 14 + Content.Server/Mobs/Commands.cs | 7 + Content.Server/Observer/Ghost.cs | 2 + Content.Server/Players/PlayerData.cs | 6 + Content.Server/ServerContentIoC.cs | 4 +- Content.Server/ServerNotifyManager.cs | 3 + .../StationEvents/StationEventCommand.cs | 21 +- Content.Shared/Administration/AdminData.cs | 39 ++ Content.Shared/Administration/AdminFlags.cs | 68 +++ .../Administration/AdminFlagsExt.cs | 73 +++ Content.Shared/CCVars.cs | 3 + .../NetMessages/MsgUpdateAdminStatus.cs | 73 +++ .../Administration/AdminFlagsExtTest.cs | 35 ++ Resources/Groups/groups.yml | 233 -------- Resources/engineCommandPerms.yml | 63 +++ SpaceStation14.sln.DotSettings | 1 + 62 files changed, 2673 insertions(+), 289 deletions(-) create mode 100644 Content.Client/Administration/ClientAdminManager.cs create mode 100644 Content.Client/Administration/IClientAdminManager.cs create mode 100644 Content.Server.Database/Migrations/Postgres/20201028210620_Admins.Designer.cs create mode 100644 Content.Server.Database/Migrations/Postgres/20201028210620_Admins.cs create mode 100644 Content.Server.Database/Migrations/Sqlite/20201028210616_Admins.Designer.cs create mode 100644 Content.Server.Database/Migrations/Sqlite/20201028210616_Admins.cs create mode 100644 Content.Server/Administration/AdminCommandAttribute.cs create mode 100644 Content.Server/Administration/AdminManager.cs create mode 100644 Content.Server/Administration/AdminRank.cs create mode 100644 Content.Server/Administration/AnyCommandAttribute.cs rename Content.Server/Administration/{ => Commands}/AGhost.cs (93%) rename Content.Server/Administration/{ => Commands}/BanCommand.cs (93%) rename Content.Server/Administration/{ => Commands}/ControlMob.cs (94%) create mode 100644 Content.Server/Administration/Commands/DeAdminCommand.cs rename Content.Server/Administration/{ => Commands}/DeleteEntitiesWithComponent.cs (93%) rename Content.Server/Administration/{ => Commands}/DeleteEntitiesWithId.cs (90%) create mode 100644 Content.Server/Administration/Commands/ReAdminCommand.cs rename Content.Server/Administration/{ => Commands}/ReadyAll.cs (90%) rename Content.Server/Administration/{ => Commands}/Rejuvenate.cs (93%) rename Content.Server/Administration/{ => Commands}/WarpCommand.cs (97%) create mode 100644 Content.Server/Administration/IAdminManager.cs create mode 100644 Content.Shared/Administration/AdminData.cs create mode 100644 Content.Shared/Administration/AdminFlags.cs create mode 100644 Content.Shared/Administration/AdminFlagsExt.cs create mode 100644 Content.Shared/Network/NetMessages/MsgUpdateAdminStatus.cs create mode 100644 Content.Tests/Shared/Administration/AdminFlagsExtTest.cs delete mode 100644 Resources/Groups/groups.yml create mode 100644 Resources/engineCommandPerms.yml diff --git a/Content.Client/Administration/ClientAdminManager.cs b/Content.Client/Administration/ClientAdminManager.cs new file mode 100644 index 0000000000..eb5bfab65e --- /dev/null +++ b/Content.Client/Administration/ClientAdminManager.cs @@ -0,0 +1,82 @@ +using System; +using System.Collections.Generic; +using Content.Shared.Administration; +using Content.Shared.Network.NetMessages; +using Robust.Client.Console; +using Robust.Shared.Interfaces.Network; +using Robust.Shared.IoC; +using Robust.Shared.Log; + +#nullable enable + +namespace Content.Client.Administration +{ + public class ClientAdminManager : IClientAdminManager, IClientConGroupImplementation, IPostInjectInit + { + [Dependency] private readonly IClientNetManager _netMgr = default!; + [Dependency] private readonly IClientConGroupController _conGroup = default!; + + private AdminData? _adminData; + private readonly HashSet _availableCommands = new HashSet(); + + public bool HasFlag(AdminFlags flag) + { + return _adminData?.HasFlag(flag) ?? false; + } + + public bool CanCommand(string cmdName) + { + return _availableCommands.Contains(cmdName); + } + + public bool CanViewVar() + { + return _adminData?.CanViewVar() ?? false; + } + + public bool CanAdminPlace() + { + return _adminData?.CanAdminPlace() ?? false; + } + + public bool CanScript() + { + return _adminData?.CanScript() ?? false; + } + + public bool CanAdminMenu() + { + return _adminData?.CanAdminMenu() ?? false; + } + + public void Initialize() + { + _netMgr.RegisterNetMessage(MsgUpdateAdminStatus.NAME, UpdateMessageRx); + } + + private void UpdateMessageRx(MsgUpdateAdminStatus message) + { + _availableCommands.Clear(); + _availableCommands.UnionWith(message.AvailableCommands); + Logger.DebugS("admin", $"Have {message.AvailableCommands.Length} commands available"); + + _adminData = message.Admin; + if (_adminData != null) + { + var flagsText = string.Join("|", AdminFlagsExt.FlagsToNames(_adminData.Flags)); + Logger.InfoS("admin", $"Updated admin status: {_adminData.Active}/{_adminData.Title}/{flagsText}"); + } + else + { + Logger.InfoS("admin", $"Updated admin status: Not admin"); + } + } + + public event Action? ConGroupUpdated; + + void IPostInjectInit.PostInject() + { + _conGroup.Implementation = this; + } + } +} diff --git a/Content.Client/Administration/IClientAdminManager.cs b/Content.Client/Administration/IClientAdminManager.cs new file mode 100644 index 0000000000..b6fca58ce2 --- /dev/null +++ b/Content.Client/Administration/IClientAdminManager.cs @@ -0,0 +1,17 @@ +using Content.Shared.Administration; + +namespace Content.Client.Administration +{ + public interface IClientAdminManager + { + bool HasFlag(AdminFlags flag); + + bool CanCommand(string cmdName); + bool CanViewVar(); + bool CanAdminPlace(); + bool CanScript(); + bool CanAdminMenu(); + + void Initialize(); + } +} diff --git a/Content.Client/ClientContentIoC.cs b/Content.Client/ClientContentIoC.cs index e7d8d90841..9e353cee87 100644 --- a/Content.Client/ClientContentIoC.cs +++ b/Content.Client/ClientContentIoC.cs @@ -1,4 +1,5 @@ -using Content.Client.Chat; +using Content.Client.Administration; +using Content.Client.Chat; using Content.Client.GameTicking; using Content.Client.Interfaces; using Content.Client.Interfaces.Chat; @@ -35,6 +36,7 @@ namespace Content.Client IoCManager.Register(); IoCManager.Register(); IoCManager.Register(); + IoCManager.Register(); } } } diff --git a/Content.Client/EntryPoint.cs b/Content.Client/EntryPoint.cs index 4eab611fc2..8512a2231a 100644 --- a/Content.Client/EntryPoint.cs +++ b/Content.Client/EntryPoint.cs @@ -1,4 +1,5 @@ using System; +using Content.Client.Administration; using Content.Client.GameObjects.Components.Actor; using Content.Client.Input; using Content.Client.Interfaces; @@ -88,6 +89,7 @@ namespace Content.Client IoCManager.BuildGraph(); + IoCManager.Resolve().Initialize(); IoCManager.Resolve().LoadParallax(); IoCManager.Resolve().PlayerJoinedServer += SubscribePlayerAttachmentEvents; IoCManager.Resolve().Initialize(); diff --git a/Content.IntegrationTests/ContentIntegrationTest.cs b/Content.IntegrationTests/ContentIntegrationTest.cs index 85184d6f65..e73a3247ca 100644 --- a/Content.IntegrationTests/ContentIntegrationTest.cs +++ b/Content.IntegrationTests/ContentIntegrationTest.cs @@ -1,5 +1,4 @@ using System; -using System.IO; using System.Threading.Tasks; using Content.Client; using Content.Client.Interfaces.Parallax; @@ -13,7 +12,6 @@ using Robust.Shared.Interfaces.Map; using Robust.Shared.Interfaces.Network; using Robust.Shared.IoC; using Robust.Shared.Map; -using Robust.Shared.Prototypes; using Robust.UnitTesting; using EntryPoint = Content.Client.EntryPoint; diff --git a/Content.Server.Database/Migrations/Postgres/20201028210620_Admins.Designer.cs b/Content.Server.Database/Migrations/Postgres/20201028210620_Admins.Designer.cs new file mode 100644 index 0000000000..ae0daea2ad --- /dev/null +++ b/Content.Server.Database/Migrations/Postgres/20201028210620_Admins.Designer.cs @@ -0,0 +1,509 @@ +// +using System; +using System.Net; +using Content.Server.Database; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +namespace Content.Server.Database.Migrations.Postgres +{ + [DbContext(typeof(PostgresServerDbContext))] + [Migration("20201028210620_Admins")] + partial class Admins + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn) + .HasAnnotation("ProductVersion", "3.1.4") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + modelBuilder.Entity("Content.Server.Database.Admin", b => + { + b.Property("UserId") + .ValueGeneratedOnAdd() + .HasColumnName("user_id") + .HasColumnType("uuid"); + + b.Property("AdminRankId") + .HasColumnName("admin_rank_id") + .HasColumnType("integer"); + + b.Property("Title") + .HasColumnName("title") + .HasColumnType("text"); + + b.HasKey("UserId"); + + b.HasIndex("AdminRankId"); + + b.ToTable("admin"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminFlag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("admin_flag_id") + .HasColumnType("integer") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("AdminId") + .HasColumnName("admin_id") + .HasColumnType("uuid"); + + b.Property("Flag") + .IsRequired() + .HasColumnName("flag") + .HasColumnType("text"); + + b.Property("Negative") + .HasColumnName("negative") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.HasIndex("AdminId"); + + b.ToTable("admin_flag"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminRank", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("admin_rank_id") + .HasColumnType("integer") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("Name") + .IsRequired() + .HasColumnName("name") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("admin_rank"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminRankFlag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("admin_rank_flag_id") + .HasColumnType("integer") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("AdminRankId") + .HasColumnName("admin_rank_id") + .HasColumnType("integer"); + + b.Property("Flag") + .IsRequired() + .HasColumnName("flag") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("AdminRankId"); + + b.ToTable("admin_rank_flag"); + }); + + modelBuilder.Entity("Content.Server.Database.Antag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("antag_id") + .HasColumnType("integer") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("AntagName") + .IsRequired() + .HasColumnName("antag_name") + .HasColumnType("text"); + + b.Property("ProfileId") + .HasColumnName("profile_id") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("ProfileId", "AntagName") + .IsUnique(); + + b.ToTable("antag"); + }); + + modelBuilder.Entity("Content.Server.Database.AssignedUserId", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("assigned_user_id_id") + .HasColumnType("integer") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("UserId") + .HasColumnName("user_id") + .HasColumnType("uuid"); + + b.Property("UserName") + .IsRequired() + .HasColumnName("user_name") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("UserId") + .IsUnique(); + + b.HasIndex("UserName") + .IsUnique(); + + b.ToTable("assigned_user_id"); + }); + + modelBuilder.Entity("Content.Server.Database.Job", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("job_id") + .HasColumnType("integer") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("JobName") + .IsRequired() + .HasColumnName("job_name") + .HasColumnType("text"); + + b.Property("Priority") + .HasColumnName("priority") + .HasColumnType("integer"); + + b.Property("ProfileId") + .HasColumnName("profile_id") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("ProfileId"); + + b.ToTable("job"); + }); + + modelBuilder.Entity("Content.Server.Database.PostgresConnectionLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("connection_log_id") + .HasColumnType("integer") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("Address") + .IsRequired() + .HasColumnName("address") + .HasColumnType("inet"); + + b.Property("Time") + .HasColumnName("time") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnName("user_id") + .HasColumnType("uuid"); + + b.Property("UserName") + .IsRequired() + .HasColumnName("user_name") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("connection_log"); + + b.HasCheckConstraint("AddressNotIPv6MappedIPv4", "NOT inet '::ffff:0.0.0.0/96' >>= address"); + }); + + modelBuilder.Entity("Content.Server.Database.PostgresPlayer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("player_id") + .HasColumnType("integer") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("FirstSeenTime") + .HasColumnName("first_seen_time") + .HasColumnType("timestamp with time zone"); + + b.Property("LastSeenAddress") + .IsRequired() + .HasColumnName("last_seen_address") + .HasColumnType("inet"); + + b.Property("LastSeenTime") + .HasColumnName("last_seen_time") + .HasColumnType("timestamp with time zone"); + + b.Property("LastSeenUserName") + .IsRequired() + .HasColumnName("last_seen_user_name") + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnName("user_id") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("player"); + + b.HasCheckConstraint("LastSeenAddressNotIPv6MappedIPv4", "NOT inet '::ffff:0.0.0.0/96' >>= last_seen_address"); + }); + + modelBuilder.Entity("Content.Server.Database.PostgresServerBan", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("server_ban_id") + .HasColumnType("integer") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property?>("Address") + .HasColumnName("address") + .HasColumnType("inet"); + + b.Property("BanTime") + .HasColumnName("ban_time") + .HasColumnType("timestamp with time zone"); + + b.Property("BanningAdmin") + .HasColumnName("banning_admin") + .HasColumnType("uuid"); + + b.Property("ExpirationTime") + .HasColumnName("expiration_time") + .HasColumnType("timestamp with time zone"); + + b.Property("Reason") + .IsRequired() + .HasColumnName("reason") + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnName("user_id") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("Address"); + + b.HasIndex("UserId"); + + b.ToTable("server_ban"); + + b.HasCheckConstraint("AddressNotIPv6MappedIPv4", "NOT inet '::ffff:0.0.0.0/96' >>= address"); + + b.HasCheckConstraint("HaveEitherAddressOrUserId", "address IS NOT NULL OR user_id IS NOT NULL"); + }); + + modelBuilder.Entity("Content.Server.Database.PostgresServerUnban", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("unban_id") + .HasColumnType("integer") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("BanId") + .HasColumnName("ban_id") + .HasColumnType("integer"); + + b.Property("UnbanTime") + .HasColumnName("unban_time") + .HasColumnType("timestamp with time zone"); + + b.Property("UnbanningAdmin") + .HasColumnName("unbanning_admin") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("BanId") + .IsUnique(); + + b.ToTable("server_unban"); + }); + + modelBuilder.Entity("Content.Server.Database.Preference", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("preference_id") + .HasColumnType("integer") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("SelectedCharacterSlot") + .HasColumnName("selected_character_slot") + .HasColumnType("integer"); + + b.Property("UserId") + .HasColumnName("user_id") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("preference"); + }); + + modelBuilder.Entity("Content.Server.Database.Profile", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("profile_id") + .HasColumnType("integer") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("Age") + .HasColumnName("age") + .HasColumnType("integer"); + + b.Property("CharacterName") + .IsRequired() + .HasColumnName("char_name") + .HasColumnType("text"); + + b.Property("EyeColor") + .IsRequired() + .HasColumnName("eye_color") + .HasColumnType("text"); + + b.Property("FacialHairColor") + .IsRequired() + .HasColumnName("facial_hair_color") + .HasColumnType("text"); + + b.Property("FacialHairName") + .IsRequired() + .HasColumnName("facial_hair_name") + .HasColumnType("text"); + + b.Property("HairColor") + .IsRequired() + .HasColumnName("hair_color") + .HasColumnType("text"); + + b.Property("HairName") + .IsRequired() + .HasColumnName("hair_name") + .HasColumnType("text"); + + b.Property("PreferenceId") + .HasColumnName("preference_id") + .HasColumnType("integer"); + + b.Property("PreferenceUnavailable") + .HasColumnName("pref_unavailable") + .HasColumnType("integer"); + + b.Property("Sex") + .IsRequired() + .HasColumnName("sex") + .HasColumnType("text"); + + b.Property("SkinColor") + .IsRequired() + .HasColumnName("skin_color") + .HasColumnType("text"); + + b.Property("Slot") + .HasColumnName("slot") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("PreferenceId"); + + b.HasIndex("Slot", "PreferenceId") + .IsUnique(); + + b.ToTable("profile"); + }); + + modelBuilder.Entity("Content.Server.Database.Admin", b => + { + b.HasOne("Content.Server.Database.AdminRank", "AdminRank") + .WithMany("Admins") + .HasForeignKey("AdminRankId") + .OnDelete(DeleteBehavior.SetNull); + }); + + modelBuilder.Entity("Content.Server.Database.AdminFlag", b => + { + b.HasOne("Content.Server.Database.Admin", "Admin") + .WithMany("Flags") + .HasForeignKey("AdminId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Content.Server.Database.AdminRankFlag", b => + { + b.HasOne("Content.Server.Database.AdminRank", "Rank") + .WithMany("Flags") + .HasForeignKey("AdminRankId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Content.Server.Database.Antag", b => + { + b.HasOne("Content.Server.Database.Profile", "Profile") + .WithMany("Antags") + .HasForeignKey("ProfileId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Content.Server.Database.Job", b => + { + b.HasOne("Content.Server.Database.Profile", "Profile") + .WithMany("Jobs") + .HasForeignKey("ProfileId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Content.Server.Database.PostgresServerUnban", b => + { + b.HasOne("Content.Server.Database.PostgresServerBan", "Ban") + .WithOne("Unban") + .HasForeignKey("Content.Server.Database.PostgresServerUnban", "BanId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Content.Server.Database.Profile", b => + { + b.HasOne("Content.Server.Database.Preference", "Preference") + .WithMany("Profiles") + .HasForeignKey("PreferenceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Content.Server.Database/Migrations/Postgres/20201028210620_Admins.cs b/Content.Server.Database/Migrations/Postgres/20201028210620_Admins.cs new file mode 100644 index 0000000000..02514652da --- /dev/null +++ b/Content.Server.Database/Migrations/Postgres/20201028210620_Admins.cs @@ -0,0 +1,115 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +namespace Content.Server.Database.Migrations.Postgres +{ + public partial class Admins : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "admin_rank", + columns: table => new + { + admin_rank_id = table.Column(nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + name = table.Column(nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_admin_rank", x => x.admin_rank_id); + }); + + migrationBuilder.CreateTable( + name: "admin", + columns: table => new + { + user_id = table.Column(nullable: false), + title = table.Column(nullable: true), + admin_rank_id = table.Column(nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_admin", x => x.user_id); + table.ForeignKey( + name: "FK_admin_admin_rank_admin_rank_id", + column: x => x.admin_rank_id, + principalTable: "admin_rank", + principalColumn: "admin_rank_id", + onDelete: ReferentialAction.SetNull); + }); + + migrationBuilder.CreateTable( + name: "admin_rank_flag", + columns: table => new + { + admin_rank_flag_id = table.Column(nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + flag = table.Column(nullable: false), + admin_rank_id = table.Column(nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_admin_rank_flag", x => x.admin_rank_flag_id); + table.ForeignKey( + name: "FK_admin_rank_flag_admin_rank_admin_rank_id", + column: x => x.admin_rank_id, + principalTable: "admin_rank", + principalColumn: "admin_rank_id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "admin_flag", + columns: table => new + { + admin_flag_id = table.Column(nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + flag = table.Column(nullable: false), + negative = table.Column(nullable: false), + admin_id = table.Column(nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_admin_flag", x => x.admin_flag_id); + table.ForeignKey( + name: "FK_admin_flag_admin_admin_id", + column: x => x.admin_id, + principalTable: "admin", + principalColumn: "user_id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_admin_admin_rank_id", + table: "admin", + column: "admin_rank_id"); + + migrationBuilder.CreateIndex( + name: "IX_admin_flag_admin_id", + table: "admin_flag", + column: "admin_id"); + + migrationBuilder.CreateIndex( + name: "IX_admin_rank_flag_admin_rank_id", + table: "admin_rank_flag", + column: "admin_rank_id"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "admin_flag"); + + migrationBuilder.DropTable( + name: "admin_rank_flag"); + + migrationBuilder.DropTable( + name: "admin"); + + migrationBuilder.DropTable( + name: "admin_rank"); + } + } +} diff --git a/Content.Server.Database/Migrations/Postgres/PostgresServerDbContextModelSnapshot.cs b/Content.Server.Database/Migrations/Postgres/PostgresServerDbContextModelSnapshot.cs index 59ae3c45f4..916c5843f2 100644 --- a/Content.Server.Database/Migrations/Postgres/PostgresServerDbContextModelSnapshot.cs +++ b/Content.Server.Database/Migrations/Postgres/PostgresServerDbContextModelSnapshot.cs @@ -20,6 +20,98 @@ namespace Content.Server.Database.Migrations.Postgres .HasAnnotation("ProductVersion", "3.1.4") .HasAnnotation("Relational:MaxIdentifierLength", 63); + modelBuilder.Entity("Content.Server.Database.Admin", b => + { + b.Property("UserId") + .ValueGeneratedOnAdd() + .HasColumnName("user_id") + .HasColumnType("uuid"); + + b.Property("AdminRankId") + .HasColumnName("admin_rank_id") + .HasColumnType("integer"); + + b.Property("Title") + .HasColumnName("title") + .HasColumnType("text"); + + b.HasKey("UserId"); + + b.HasIndex("AdminRankId"); + + b.ToTable("admin"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminFlag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("admin_flag_id") + .HasColumnType("integer") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("AdminId") + .HasColumnName("admin_id") + .HasColumnType("uuid"); + + b.Property("Flag") + .IsRequired() + .HasColumnName("flag") + .HasColumnType("text"); + + b.Property("Negative") + .HasColumnName("negative") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.HasIndex("AdminId"); + + b.ToTable("admin_flag"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminRank", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("admin_rank_id") + .HasColumnType("integer") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("Name") + .IsRequired() + .HasColumnName("name") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("admin_rank"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminRankFlag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("admin_rank_flag_id") + .HasColumnType("integer") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("AdminRankId") + .HasColumnName("admin_rank_id") + .HasColumnType("integer"); + + b.Property("Flag") + .IsRequired() + .HasColumnName("flag") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("AdminRankId"); + + b.ToTable("admin_rank_flag"); + }); + modelBuilder.Entity("Content.Server.Database.Antag", b => { b.Property("Id") @@ -348,6 +440,32 @@ namespace Content.Server.Database.Migrations.Postgres b.ToTable("profile"); }); + modelBuilder.Entity("Content.Server.Database.Admin", b => + { + b.HasOne("Content.Server.Database.AdminRank", "AdminRank") + .WithMany("Admins") + .HasForeignKey("AdminRankId") + .OnDelete(DeleteBehavior.SetNull); + }); + + modelBuilder.Entity("Content.Server.Database.AdminFlag", b => + { + b.HasOne("Content.Server.Database.Admin", "Admin") + .WithMany("Flags") + .HasForeignKey("AdminId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Content.Server.Database.AdminRankFlag", b => + { + b.HasOne("Content.Server.Database.AdminRank", "Rank") + .WithMany("Flags") + .HasForeignKey("AdminRankId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + modelBuilder.Entity("Content.Server.Database.Antag", b => { b.HasOne("Content.Server.Database.Profile", "Profile") diff --git a/Content.Server.Database/Migrations/Sqlite/20201028210616_Admins.Designer.cs b/Content.Server.Database/Migrations/Sqlite/20201028210616_Admins.Designer.cs new file mode 100644 index 0000000000..23ebe0d41b --- /dev/null +++ b/Content.Server.Database/Migrations/Sqlite/20201028210616_Admins.Designer.cs @@ -0,0 +1,476 @@ +// +using System; +using Content.Server.Database; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +namespace Content.Server.Database.Migrations.Sqlite +{ + [DbContext(typeof(SqliteServerDbContext))] + [Migration("20201028210616_Admins")] + partial class Admins + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "3.1.4"); + + modelBuilder.Entity("Content.Server.Database.Admin", b => + { + b.Property("UserId") + .ValueGeneratedOnAdd() + .HasColumnName("user_id") + .HasColumnType("TEXT"); + + b.Property("AdminRankId") + .HasColumnName("admin_rank_id") + .HasColumnType("INTEGER"); + + b.Property("Title") + .HasColumnName("title") + .HasColumnType("TEXT"); + + b.HasKey("UserId"); + + b.HasIndex("AdminRankId"); + + b.ToTable("admin"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminFlag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("admin_flag_id") + .HasColumnType("INTEGER"); + + b.Property("AdminId") + .HasColumnName("admin_id") + .HasColumnType("TEXT"); + + b.Property("Flag") + .IsRequired() + .HasColumnName("flag") + .HasColumnType("TEXT"); + + b.Property("Negative") + .HasColumnName("negative") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AdminId"); + + b.ToTable("admin_flag"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminRank", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("admin_rank_id") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasColumnName("name") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("admin_rank"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminRankFlag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("admin_rank_flag_id") + .HasColumnType("INTEGER"); + + b.Property("AdminRankId") + .HasColumnName("admin_rank_id") + .HasColumnType("INTEGER"); + + b.Property("Flag") + .IsRequired() + .HasColumnName("flag") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("AdminRankId"); + + b.ToTable("admin_rank_flag"); + }); + + modelBuilder.Entity("Content.Server.Database.Antag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("antag_id") + .HasColumnType("INTEGER"); + + b.Property("AntagName") + .IsRequired() + .HasColumnName("antag_name") + .HasColumnType("TEXT"); + + b.Property("ProfileId") + .HasColumnName("profile_id") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ProfileId", "AntagName") + .IsUnique(); + + b.ToTable("antag"); + }); + + modelBuilder.Entity("Content.Server.Database.AssignedUserId", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("assigned_user_id_id") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnName("user_id") + .HasColumnType("TEXT"); + + b.Property("UserName") + .IsRequired() + .HasColumnName("user_name") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId") + .IsUnique(); + + b.HasIndex("UserName") + .IsUnique(); + + b.ToTable("assigned_user_id"); + }); + + modelBuilder.Entity("Content.Server.Database.Job", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("job_id") + .HasColumnType("INTEGER"); + + b.Property("JobName") + .IsRequired() + .HasColumnName("job_name") + .HasColumnType("TEXT"); + + b.Property("Priority") + .HasColumnName("priority") + .HasColumnType("INTEGER"); + + b.Property("ProfileId") + .HasColumnName("profile_id") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ProfileId"); + + b.ToTable("job"); + }); + + modelBuilder.Entity("Content.Server.Database.Preference", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("preference_id") + .HasColumnType("INTEGER"); + + b.Property("SelectedCharacterSlot") + .HasColumnName("selected_character_slot") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnName("user_id") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("preference"); + }); + + modelBuilder.Entity("Content.Server.Database.Profile", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("profile_id") + .HasColumnType("INTEGER"); + + b.Property("Age") + .HasColumnName("age") + .HasColumnType("INTEGER"); + + b.Property("CharacterName") + .IsRequired() + .HasColumnName("char_name") + .HasColumnType("TEXT"); + + b.Property("EyeColor") + .IsRequired() + .HasColumnName("eye_color") + .HasColumnType("TEXT"); + + b.Property("FacialHairColor") + .IsRequired() + .HasColumnName("facial_hair_color") + .HasColumnType("TEXT"); + + b.Property("FacialHairName") + .IsRequired() + .HasColumnName("facial_hair_name") + .HasColumnType("TEXT"); + + b.Property("HairColor") + .IsRequired() + .HasColumnName("hair_color") + .HasColumnType("TEXT"); + + b.Property("HairName") + .IsRequired() + .HasColumnName("hair_name") + .HasColumnType("TEXT"); + + b.Property("PreferenceId") + .HasColumnName("preference_id") + .HasColumnType("INTEGER"); + + b.Property("PreferenceUnavailable") + .HasColumnName("pref_unavailable") + .HasColumnType("INTEGER"); + + b.Property("Sex") + .IsRequired() + .HasColumnName("sex") + .HasColumnType("TEXT"); + + b.Property("SkinColor") + .IsRequired() + .HasColumnName("skin_color") + .HasColumnType("TEXT"); + + b.Property("Slot") + .HasColumnName("slot") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("PreferenceId"); + + b.HasIndex("Slot", "PreferenceId") + .IsUnique(); + + b.ToTable("profile"); + }); + + modelBuilder.Entity("Content.Server.Database.SqliteConnectionLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("connection_log_id") + .HasColumnType("INTEGER"); + + b.Property("Address") + .IsRequired() + .HasColumnName("address") + .HasColumnType("TEXT"); + + b.Property("Time") + .HasColumnName("time") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnName("user_id") + .HasColumnType("TEXT"); + + b.Property("UserName") + .IsRequired() + .HasColumnName("user_name") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("connection_log"); + }); + + modelBuilder.Entity("Content.Server.Database.SqlitePlayer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("player_id") + .HasColumnType("INTEGER"); + + b.Property("FirstSeenTime") + .HasColumnName("first_seen_time") + .HasColumnType("TEXT"); + + b.Property("LastSeenAddress") + .IsRequired() + .HasColumnName("last_seen_address") + .HasColumnType("TEXT"); + + b.Property("LastSeenTime") + .HasColumnName("last_seen_time") + .HasColumnType("TEXT"); + + b.Property("LastSeenUserName") + .IsRequired() + .HasColumnName("last_seen_user_name") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnName("user_id") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("player"); + }); + + modelBuilder.Entity("Content.Server.Database.SqliteServerBan", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("ban_id") + .HasColumnType("INTEGER"); + + b.Property("Address") + .HasColumnName("address") + .HasColumnType("TEXT"); + + b.Property("BanTime") + .HasColumnName("ban_time") + .HasColumnType("TEXT"); + + b.Property("BanningAdmin") + .HasColumnName("banning_admin") + .HasColumnType("TEXT"); + + b.Property("ExpirationTime") + .HasColumnName("expiration_time") + .HasColumnType("TEXT"); + + b.Property("Reason") + .IsRequired() + .HasColumnName("reason") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnName("user_id") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("ban"); + }); + + modelBuilder.Entity("Content.Server.Database.SqliteServerUnban", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("unban_id") + .HasColumnType("INTEGER"); + + b.Property("BanId") + .HasColumnName("ban_id") + .HasColumnType("INTEGER"); + + b.Property("UnbanTime") + .HasColumnName("unban_time") + .HasColumnType("TEXT"); + + b.Property("UnbanningAdmin") + .HasColumnName("unbanning_admin") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("BanId") + .IsUnique(); + + b.ToTable("unban"); + }); + + modelBuilder.Entity("Content.Server.Database.Admin", b => + { + b.HasOne("Content.Server.Database.AdminRank", "AdminRank") + .WithMany("Admins") + .HasForeignKey("AdminRankId") + .OnDelete(DeleteBehavior.SetNull); + }); + + modelBuilder.Entity("Content.Server.Database.AdminFlag", b => + { + b.HasOne("Content.Server.Database.Admin", "Admin") + .WithMany("Flags") + .HasForeignKey("AdminId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Content.Server.Database.AdminRankFlag", b => + { + b.HasOne("Content.Server.Database.AdminRank", "Rank") + .WithMany("Flags") + .HasForeignKey("AdminRankId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Content.Server.Database.Antag", b => + { + b.HasOne("Content.Server.Database.Profile", "Profile") + .WithMany("Antags") + .HasForeignKey("ProfileId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Content.Server.Database.Job", b => + { + b.HasOne("Content.Server.Database.Profile", "Profile") + .WithMany("Jobs") + .HasForeignKey("ProfileId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Content.Server.Database.Profile", b => + { + b.HasOne("Content.Server.Database.Preference", "Preference") + .WithMany("Profiles") + .HasForeignKey("PreferenceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Content.Server.Database.SqliteServerUnban", b => + { + b.HasOne("Content.Server.Database.SqliteServerBan", "Ban") + .WithOne("Unban") + .HasForeignKey("Content.Server.Database.SqliteServerUnban", "BanId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Content.Server.Database/Migrations/Sqlite/20201028210616_Admins.cs b/Content.Server.Database/Migrations/Sqlite/20201028210616_Admins.cs new file mode 100644 index 0000000000..c9c65034d4 --- /dev/null +++ b/Content.Server.Database/Migrations/Sqlite/20201028210616_Admins.cs @@ -0,0 +1,114 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace Content.Server.Database.Migrations.Sqlite +{ + public partial class Admins : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "admin_rank", + columns: table => new + { + admin_rank_id = table.Column(nullable: false) + .Annotation("Sqlite:Autoincrement", true), + name = table.Column(nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_admin_rank", x => x.admin_rank_id); + }); + + migrationBuilder.CreateTable( + name: "admin", + columns: table => new + { + user_id = table.Column(nullable: false), + title = table.Column(nullable: true), + admin_rank_id = table.Column(nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_admin", x => x.user_id); + table.ForeignKey( + name: "FK_admin_admin_rank_admin_rank_id", + column: x => x.admin_rank_id, + principalTable: "admin_rank", + principalColumn: "admin_rank_id", + onDelete: ReferentialAction.SetNull); + }); + + migrationBuilder.CreateTable( + name: "admin_rank_flag", + columns: table => new + { + admin_rank_flag_id = table.Column(nullable: false) + .Annotation("Sqlite:Autoincrement", true), + flag = table.Column(nullable: false), + admin_rank_id = table.Column(nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_admin_rank_flag", x => x.admin_rank_flag_id); + table.ForeignKey( + name: "FK_admin_rank_flag_admin_rank_admin_rank_id", + column: x => x.admin_rank_id, + principalTable: "admin_rank", + principalColumn: "admin_rank_id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "admin_flag", + columns: table => new + { + admin_flag_id = table.Column(nullable: false) + .Annotation("Sqlite:Autoincrement", true), + flag = table.Column(nullable: false), + negative = table.Column(nullable: false), + admin_id = table.Column(nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_admin_flag", x => x.admin_flag_id); + table.ForeignKey( + name: "FK_admin_flag_admin_admin_id", + column: x => x.admin_id, + principalTable: "admin", + principalColumn: "user_id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_admin_admin_rank_id", + table: "admin", + column: "admin_rank_id"); + + migrationBuilder.CreateIndex( + name: "IX_admin_flag_admin_id", + table: "admin_flag", + column: "admin_id"); + + migrationBuilder.CreateIndex( + name: "IX_admin_rank_flag_admin_rank_id", + table: "admin_rank_flag", + column: "admin_rank_id"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "admin_flag"); + + migrationBuilder.DropTable( + name: "admin_rank_flag"); + + migrationBuilder.DropTable( + name: "admin"); + + migrationBuilder.DropTable( + name: "admin_rank"); + } + } +} diff --git a/Content.Server.Database/Migrations/Sqlite/SqliteServerDbContextModelSnapshot.cs b/Content.Server.Database/Migrations/Sqlite/SqliteServerDbContextModelSnapshot.cs index b00a7e32c7..67f92bea49 100644 --- a/Content.Server.Database/Migrations/Sqlite/SqliteServerDbContextModelSnapshot.cs +++ b/Content.Server.Database/Migrations/Sqlite/SqliteServerDbContextModelSnapshot.cs @@ -16,6 +16,95 @@ namespace Content.Server.Database.Migrations.Sqlite modelBuilder .HasAnnotation("ProductVersion", "3.1.4"); + modelBuilder.Entity("Content.Server.Database.Admin", b => + { + b.Property("UserId") + .ValueGeneratedOnAdd() + .HasColumnName("user_id") + .HasColumnType("TEXT"); + + b.Property("AdminRankId") + .HasColumnName("admin_rank_id") + .HasColumnType("INTEGER"); + + b.Property("Title") + .HasColumnName("title") + .HasColumnType("TEXT"); + + b.HasKey("UserId"); + + b.HasIndex("AdminRankId"); + + b.ToTable("admin"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminFlag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("admin_flag_id") + .HasColumnType("INTEGER"); + + b.Property("AdminId") + .HasColumnName("admin_id") + .HasColumnType("TEXT"); + + b.Property("Flag") + .IsRequired() + .HasColumnName("flag") + .HasColumnType("TEXT"); + + b.Property("Negative") + .HasColumnName("negative") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AdminId"); + + b.ToTable("admin_flag"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminRank", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("admin_rank_id") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasColumnName("name") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("admin_rank"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminRankFlag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("admin_rank_flag_id") + .HasColumnType("INTEGER"); + + b.Property("AdminRankId") + .HasColumnName("admin_rank_id") + .HasColumnType("INTEGER"); + + b.Property("Flag") + .IsRequired() + .HasColumnName("flag") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("AdminRankId"); + + b.ToTable("admin_rank_flag"); + }); + modelBuilder.Entity("Content.Server.Database.Antag", b => { b.Property("Id") @@ -318,6 +407,32 @@ namespace Content.Server.Database.Migrations.Sqlite b.ToTable("unban"); }); + modelBuilder.Entity("Content.Server.Database.Admin", b => + { + b.HasOne("Content.Server.Database.AdminRank", "AdminRank") + .WithMany("Admins") + .HasForeignKey("AdminRankId") + .OnDelete(DeleteBehavior.SetNull); + }); + + modelBuilder.Entity("Content.Server.Database.AdminFlag", b => + { + b.HasOne("Content.Server.Database.Admin", "Admin") + .WithMany("Flags") + .HasForeignKey("AdminId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Content.Server.Database.AdminRankFlag", b => + { + b.HasOne("Content.Server.Database.AdminRank", "Rank") + .WithMany("Flags") + .HasForeignKey("AdminRankId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + modelBuilder.Entity("Content.Server.Database.Antag", b => { b.HasOne("Content.Server.Database.Profile", "Profile") diff --git a/Content.Server.Database/Model.cs b/Content.Server.Database/Model.cs index 5aa4f6ec81..d701c5cfcc 100644 --- a/Content.Server.Database/Model.cs +++ b/Content.Server.Database/Model.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; using Microsoft.EntityFrameworkCore; @@ -26,6 +27,8 @@ namespace Content.Server.Database public DbSet Preference { get; set; } = null!; public DbSet Profile { get; set; } = null!; public DbSet AssignedUserId { get; set; } = null!; + public DbSet Admin { get; set; } = null!; + public DbSet AdminRank { get; set; } = null!; protected override void OnModelCreating(ModelBuilder modelBuilder) { @@ -49,6 +52,11 @@ namespace Content.Server.Database modelBuilder.Entity() .HasIndex(p => p.UserId) .IsUnique(); + + modelBuilder.Entity() + .HasOne(p => p.AdminRank) + .WithMany(p => p!.Admins) + .OnDelete(DeleteBehavior.SetNull); } } @@ -135,4 +143,46 @@ namespace Content.Server.Database [Column("user_id")] public Guid UserId { get; set; } } + + [Table("admin")] + public class Admin + { + [Column("user_id"), Key] public Guid UserId { get; set; } + [Column("title")] public string? Title { get; set; } + + [Column("admin_rank_id")] public int? AdminRankId { get; set; } + public AdminRank? AdminRank { get; set; } + public List Flags { get; set; } = default!; + } + + [Table("admin_flag")] + public class AdminFlag + { + [Column("admin_flag_id")] public int Id { get; set; } + [Column("flag")] public string Flag { get; set; } = default!; + [Column("negative")] public bool Negative { get; set; } + + [Column("admin_id")] public Guid AdminId { get; set; } + public Admin Admin { get; set; } = default!; + } + + [Table("admin_rank")] + public class AdminRank + { + [Column("admin_rank_id")] public int Id { get; set; } + [Column("name")] public string Name { get; set; } = default!; + + public List Admins { get; set; } = default!; + public List Flags { get; set; } = default!; + } + + [Table("admin_rank_flag")] + public class AdminRankFlag + { + [Column("admin_rank_flag_id")] public int Id { get; set; } + [Column("flag")] public string Flag { get; set; } = default!; + + [Column("admin_rank_id")] public int AdminRankId { get; set; } + public AdminRank Rank { get; set; } = default!; + } } diff --git a/Content.Server/Administration/AdminCommandAttribute.cs b/Content.Server/Administration/AdminCommandAttribute.cs new file mode 100644 index 0000000000..79ae7fc769 --- /dev/null +++ b/Content.Server/Administration/AdminCommandAttribute.cs @@ -0,0 +1,27 @@ +using System; +using Content.Shared.Administration; +using JetBrains.Annotations; +using Robust.Server.Interfaces.Console; + +namespace Content.Server.Administration +{ + /// + /// Specifies that a command can only be executed by an admin with the specified flags. + /// + /// + /// If this attribute is used multiple times, either attribute's flag sets can be used to get access. + /// + /// + [AttributeUsage(AttributeTargets.Class, AllowMultiple = true)] + [BaseTypeRequired(typeof(IClientCommand))] + [MeansImplicitUse] + public sealed class AdminCommandAttribute : Attribute + { + public AdminCommandAttribute(AdminFlags flags) + { + Flags = flags; + } + + public AdminFlags Flags { get; } + } +} diff --git a/Content.Server/Administration/AdminManager.cs b/Content.Server/Administration/AdminManager.cs new file mode 100644 index 0000000000..1b4862bfe6 --- /dev/null +++ b/Content.Server/Administration/AdminManager.cs @@ -0,0 +1,377 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net; +using System.Reflection; +using Content.Server.Database; +using Content.Server.Players; +using Content.Shared; +using Content.Shared.Administration; +using Content.Shared.Network.NetMessages; +using Robust.Server.Console; +using Robust.Server.Interfaces.Console; +using Robust.Server.Interfaces.Player; +using Robust.Server.Player; +using Robust.Shared.Enums; +using Robust.Shared.Interfaces.Configuration; +using Robust.Shared.Interfaces.Network; +using Robust.Shared.Interfaces.Resources; +using Robust.Shared.IoC; +using Robust.Shared.Localization; +using Robust.Shared.Utility; +using YamlDotNet.RepresentationModel; + +#nullable enable + +namespace Content.Server.Administration +{ + public sealed class AdminManager : IAdminManager, IPostInjectInit, IConGroupControllerImplementation + { + [Dependency] private readonly IPlayerManager _playerManager = default!; + [Dependency] private readonly IServerDbManager _dbManager = default!; + [Dependency] private readonly IConfigurationManager _cfg = default!; + [Dependency] private readonly IServerNetManager _netMgr = default!; + [Dependency] private readonly IConGroupController _conGroup = default!; + [Dependency] private readonly IResourceManager _res = default!; + [Dependency] private readonly IConsoleShell _consoleShell = default!; + + private readonly Dictionary _admins = new Dictionary(); + + public IEnumerable ActiveAdmins => _admins + .Where(p => p.Value.Data.Active) + .Select(p => p.Key); + + // If a command isn't in this list it's server-console only. + // if a command is in but the flags value is null it's available to everybody. + private readonly HashSet _anyCommands = new HashSet(); + private readonly Dictionary _adminCommands = new Dictionary(); + + public AdminData? GetAdminData(IPlayerSession session, bool includeDeAdmin = false) + { + if (_admins.TryGetValue(session, out var reg) && (reg.Data.Active || includeDeAdmin)) + { + return reg.Data; + } + + return null; + } + + public void DeAdmin(IPlayerSession session) + { + if (!_admins.TryGetValue(session, out var reg)) + { + throw new ArgumentException($"Player {session} is not an admin"); + } + + if (!reg.Data.Active) + { + return; + } + + var plyData = session.ContentData()!; + plyData.ExplicitlyDeadminned = true; + reg.Data.Active = false; + + // TODO: Send messages to all admins. + + UpdateAdminStatus(session); + } + + public void ReAdmin(IPlayerSession session) + { + if (!_admins.TryGetValue(session, out var reg)) + { + throw new ArgumentException($"Player {session} is not an admin"); + } + + var plyData = session.ContentData()!; + plyData.ExplicitlyDeadminned = true; + reg.Data.Active = true; + + // TODO: Send messages to all admins. + + UpdateAdminStatus(session); + } + + public void Initialize() + { + _netMgr.RegisterNetMessage(MsgUpdateAdminStatus.NAME); + + // Cache permissions for loaded console commands with the requisite attributes. + foreach (var (cmdName, cmd) in _consoleShell.AvailableCommands) + { + var (isAvail, flagsReq) = GetRequiredFlag(cmd); + + if (!isAvail) + { + continue; + } + + if (flagsReq.Length != 0) + { + _adminCommands.Add(cmdName, flagsReq); + } + else + { + _anyCommands.Add(cmdName); + } + } + + // Load flags for engine commands, since those don't have the attributes. + if (_res.TryContentFileRead(new ResourcePath("/engineCommandPerms.yml"), out var fs)) + { + using var reader = new StreamReader(fs, EncodingHelpers.UTF8); + var yStream = new YamlStream(); + yStream.Load(reader); + var root = (YamlSequenceNode) yStream.Documents[0].RootNode; + + foreach (var child in root) + { + var map = (YamlMappingNode) child; + var commands = map.GetNode("Commands").Select(p => p.AsString()); + if (map.TryGetNode("Flags", out var flagsNode)) + { + var flagNames = flagsNode.AsString().Split(",", StringSplitOptions.RemoveEmptyEntries); + var flags = AdminFlagsExt.NamesToFlags(flagNames); + foreach (var cmd in commands) + { + if (!_adminCommands.TryGetValue(cmd, out var exFlags)) + { + _adminCommands.Add(cmd, new []{flags}); + } + else + { + var newArr = new AdminFlags[exFlags.Length + 1]; + exFlags.CopyTo(newArr, 0); + exFlags[^1] = flags; + _adminCommands[cmd] = newArr; + } + } + } + else + { + _anyCommands.UnionWith(commands); + } + } + } + } + + void IPostInjectInit.PostInject() + { + _playerManager.PlayerStatusChanged += PlayerStatusChanged; + _conGroup.Implementation = this; + } + + // NOTE: Also sends commands list for non admins.. + private void UpdateAdminStatus(IPlayerSession session) + { + var msg = _netMgr.CreateNetMessage(); + + var commands = new List(_anyCommands); + + if (_admins.TryGetValue(session, out var adminData)) + { + msg.Admin = adminData.Data; + + commands.AddRange(_adminCommands + .Where(p => p.Value.Any(f => adminData.Data.HasFlag(f))) + .Select(p => p.Key)); + } + + msg.AvailableCommands = commands.ToArray(); + + _netMgr.ServerSendMessage(msg, session.ConnectedClient); + } + + private void PlayerStatusChanged(object? sender, SessionStatusEventArgs e) + { + if (e.NewStatus == SessionStatus.Connected) + { + // Run this so that available commands list gets sent. + UpdateAdminStatus(e.Session); + } + else if (e.NewStatus == SessionStatus.InGame) + { + LoginAdminMaybe(e.Session); + } + else if (e.NewStatus == SessionStatus.Disconnected) + { + _admins.Remove(e.Session); + } + } + + private async void LoginAdminMaybe(IPlayerSession session) + { + AdminReg reg; + if (IsLocal(session) && _cfg.GetCVar(CCVars.ConsoleLoginLocal)) + { + var data = new AdminData + { + Title = Loc.GetString("Host"), + Flags = AdminFlagsExt.Everything, + }; + + reg = new AdminReg(session, data) + { + IsSpecialLogin = true, + }; + } + else + { + var dbData = await _dbManager.GetAdminDataForAsync(session.UserId); + + if (dbData == null) + { + // Not an admin! + return; + } + + var flags = AdminFlags.None; + + if (dbData.AdminRank != null) + { + flags = AdminFlagsExt.NamesToFlags(dbData.AdminRank.Flags.Select(p => p.Flag)); + } + + foreach (var dbFlag in dbData.Flags) + { + var flag = AdminFlagsExt.NameToFlag(dbFlag.Flag); + if (dbFlag.Negative) + { + flags &= ~flag; + } + else + { + flags |= flag; + } + } + + var data = new AdminData + { + Flags = flags + }; + + if (dbData.Title != null) + { + data.Title = dbData.Title; + } + else if (dbData.AdminRank != null) + { + data.Title = dbData.AdminRank.Name; + } + + reg = new AdminReg(session, data); + } + + _admins.Add(session, reg); + + if (!session.ContentData()!.ExplicitlyDeadminned) + { + reg.Data.Active = true; + } + + UpdateAdminStatus(session); + } + + private static bool IsLocal(IPlayerSession player) + { + var ep = player.ConnectedClient.RemoteEndPoint; + var addr = ep.Address; + if (addr.IsIPv4MappedToIPv6) + { + addr = addr.MapToIPv4(); + } + + return Equals(addr, IPAddress.Loopback) || Equals(addr, IPAddress.IPv6Loopback); + } + + public bool CanCommand(IPlayerSession session, string cmdName) + { + if (_anyCommands.Contains(cmdName)) + { + // Anybody can use this command. + return true; + } + + if (!_adminCommands.TryGetValue(cmdName, out var flagsReq)) + { + // Server-console only. + return false; + } + + var data = GetAdminData(session); + if (data == null) + { + // Player isn't an admin. + return false; + } + + foreach (var flagReq in flagsReq) + { + if (data.HasFlag(flagReq)) + { + return true; + } + } + + return false; + } + + private static (bool isAvail, AdminFlags[] flagsReq) GetRequiredFlag(IClientCommand cmd) + { + var type = cmd.GetType(); + if (Attribute.IsDefined(type, typeof(AnyCommandAttribute))) + { + // Available to everybody. + return (true, Array.Empty()); + } + + var attribs = type.GetCustomAttributes(typeof(AdminCommandAttribute)) + .Cast() + .Select(p => p.Flags) + .ToArray(); + + // If attribs.length == 0 then no access attribute is specified, + // and this is a server-only command. + return (attribs.Length != 0, attribs); + } + + public bool CanViewVar(IPlayerSession session) + { + return GetAdminData(session)?.CanViewVar() ?? false; + } + + public bool CanAdminPlace(IPlayerSession session) + { + return GetAdminData(session)?.CanAdminPlace() ?? false; + } + + public bool CanScript(IPlayerSession session) + { + return GetAdminData(session)?.CanScript() ?? false; + } + + public bool CanAdminMenu(IPlayerSession session) + { + return GetAdminData(session)?.CanAdminMenu() ?? false; + } + + private sealed class AdminReg + { + public IPlayerSession Session; + + public AdminData Data; + + // Such as console.loginlocal + // Means that stuff like permissions editing is blocked. + public bool IsSpecialLogin; + + public AdminReg(IPlayerSession session, AdminData data) + { + Data = data; + Session = session; + } + } + } +} diff --git a/Content.Server/Administration/AdminRank.cs b/Content.Server/Administration/AdminRank.cs new file mode 100644 index 0000000000..925022498e --- /dev/null +++ b/Content.Server/Administration/AdminRank.cs @@ -0,0 +1,18 @@ +using Content.Shared.Administration; + +#nullable enable + +namespace Content.Server.Administration +{ + public sealed class AdminRank + { + public AdminRank(string name, AdminFlags flags) + { + Name = name; + Flags = flags; + } + + public string Name { get; } + public AdminFlags Flags { get; } + } +} diff --git a/Content.Server/Administration/AnyCommandAttribute.cs b/Content.Server/Administration/AnyCommandAttribute.cs new file mode 100644 index 0000000000..ab9895f728 --- /dev/null +++ b/Content.Server/Administration/AnyCommandAttribute.cs @@ -0,0 +1,18 @@ +using System; +using JetBrains.Annotations; +using Robust.Server.Interfaces.Console; + +namespace Content.Server.Administration +{ + /// + /// Specifies that a command can be executed by any player. + /// + /// + [AttributeUsage(AttributeTargets.Class)] + [BaseTypeRequired(typeof(IClientCommand))] + [MeansImplicitUse] + public sealed class AnyCommandAttribute : Attribute + { + + } +} diff --git a/Content.Server/Administration/AGhost.cs b/Content.Server/Administration/Commands/AGhost.cs similarity index 93% rename from Content.Server/Administration/AGhost.cs rename to Content.Server/Administration/Commands/AGhost.cs index 5226c4637f..89c71d0346 100644 --- a/Content.Server/Administration/AGhost.cs +++ b/Content.Server/Administration/Commands/AGhost.cs @@ -1,12 +1,14 @@ using Content.Server.GameObjects.Components.Observer; using Content.Server.Players; +using Content.Shared.Administration; using Robust.Server.Interfaces.Console; using Robust.Server.Interfaces.Player; using Robust.Shared.Interfaces.GameObjects; using Robust.Shared.IoC; -namespace Content.Server.Administration +namespace Content.Server.Administration.Commands { + [AdminCommand(AdminFlags.Admin)] public class AGhost : IClientCommand { public string Command => "aghost"; diff --git a/Content.Server/Administration/BanCommand.cs b/Content.Server/Administration/Commands/BanCommand.cs similarity index 93% rename from Content.Server/Administration/BanCommand.cs rename to Content.Server/Administration/Commands/BanCommand.cs index 2dbabd2302..f542d1c0c2 100644 --- a/Content.Server/Administration/BanCommand.cs +++ b/Content.Server/Administration/Commands/BanCommand.cs @@ -1,6 +1,6 @@ using System; -using System.Net; using Content.Server.Database; +using Content.Shared.Administration; using Robust.Server.Interfaces.Console; using Robust.Server.Interfaces.Player; using Robust.Shared.IoC; @@ -8,8 +8,9 @@ using Robust.Shared.Network; #nullable enable -namespace Content.Server.Administration +namespace Content.Server.Administration.Commands { + [AdminCommand(AdminFlags.Ban)] public sealed class BanCommand : IClientCommand { public string Command => "ban"; diff --git a/Content.Server/Administration/ControlMob.cs b/Content.Server/Administration/Commands/ControlMob.cs similarity index 94% rename from Content.Server/Administration/ControlMob.cs rename to Content.Server/Administration/Commands/ControlMob.cs index 317f2d77ee..1e979e633a 100644 --- a/Content.Server/Administration/ControlMob.cs +++ b/Content.Server/Administration/Commands/ControlMob.cs @@ -1,6 +1,7 @@ using Content.Server.GameObjects.Components.Mobs; using Content.Server.GameObjects.Components.Observer; using Content.Server.Players; +using Content.Shared.Administration; using Robust.Server.Interfaces.Console; using Robust.Server.Interfaces.Player; using Robust.Shared.GameObjects; @@ -8,8 +9,9 @@ using Robust.Shared.Interfaces.GameObjects; using Robust.Shared.IoC; using Robust.Shared.Localization; -namespace Content.Server.Administration +namespace Content.Server.Administration.Commands { + [AdminCommand(AdminFlags.Admin)] class ControlMob : IClientCommand { public string Command => "controlmob"; diff --git a/Content.Server/Administration/Commands/DeAdminCommand.cs b/Content.Server/Administration/Commands/DeAdminCommand.cs new file mode 100644 index 0000000000..44135a0c1b --- /dev/null +++ b/Content.Server/Administration/Commands/DeAdminCommand.cs @@ -0,0 +1,31 @@ +using Content.Shared.Administration; +using JetBrains.Annotations; +using Robust.Server.Interfaces.Console; +using Robust.Server.Interfaces.Player; +using Robust.Shared.IoC; + +#nullable enable + +namespace Content.Server.Administration +{ + [UsedImplicitly] + [AdminCommand(AdminFlags.None)] + public class DeAdminCommand : IClientCommand + { + public string Command => "deadmin"; + public string Description => "Temporarily de-admins you so you can experience the round as a normal player."; + public string Help => "Usage: deadmin\nUse readmin to re-admin after using this."; + + public void Execute(IConsoleShell shell, IPlayerSession? player, string[] args) + { + if (player == null) + { + shell.SendText(player, "You cannot use this command from the server console."); + return; + } + + var mgr = IoCManager.Resolve(); + mgr.DeAdmin(player); + } + } +} diff --git a/Content.Server/Administration/DeleteEntitiesWithComponent.cs b/Content.Server/Administration/Commands/DeleteEntitiesWithComponent.cs similarity index 93% rename from Content.Server/Administration/DeleteEntitiesWithComponent.cs rename to Content.Server/Administration/Commands/DeleteEntitiesWithComponent.cs index 0df4af06b8..f87bd7a99b 100644 --- a/Content.Server/Administration/DeleteEntitiesWithComponent.cs +++ b/Content.Server/Administration/Commands/DeleteEntitiesWithComponent.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using Content.Shared.Administration; using Robust.Server.Interfaces.Console; using Robust.Server.Interfaces.Player; using Robust.Shared.GameObjects; @@ -7,8 +8,9 @@ using Robust.Shared.Interfaces.GameObjects; using Robust.Shared.IoC; using Robust.Shared.Localization; -namespace Content.Server.Administration +namespace Content.Server.Administration.Commands { + [AdminCommand(AdminFlags.Admin)] class DeleteEntitiesWithComponent : IClientCommand { public string Command => "deleteewc"; diff --git a/Content.Server/Administration/DeleteEntitiesWithId.cs b/Content.Server/Administration/Commands/DeleteEntitiesWithId.cs similarity index 90% rename from Content.Server/Administration/DeleteEntitiesWithId.cs rename to Content.Server/Administration/Commands/DeleteEntitiesWithId.cs index b2f3448167..d7ff635c10 100644 --- a/Content.Server/Administration/DeleteEntitiesWithId.cs +++ b/Content.Server/Administration/Commands/DeleteEntitiesWithId.cs @@ -1,12 +1,14 @@ #nullable enable +using Content.Shared.Administration; using Robust.Server.Interfaces.Console; using Robust.Server.Interfaces.Player; using Robust.Shared.GameObjects; using Robust.Shared.Interfaces.GameObjects; using Robust.Shared.IoC; -namespace Content.Server.Administration +namespace Content.Server.Administration.Commands { + [AdminCommand(AdminFlags.Admin)] public class DeleteEntitiesWithId : IClientCommand { public string Command => "deleteewi"; diff --git a/Content.Server/Administration/Commands/ReAdminCommand.cs b/Content.Server/Administration/Commands/ReAdminCommand.cs new file mode 100644 index 0000000000..edd197b71f --- /dev/null +++ b/Content.Server/Administration/Commands/ReAdminCommand.cs @@ -0,0 +1,35 @@ +using Robust.Server.Interfaces.Console; +using Robust.Server.Interfaces.Player; +using Robust.Shared.IoC; + +#nullable enable + +namespace Content.Server.Administration +{ + [AnyCommand] + public class ReAdminCommand : IClientCommand + { + public string Command => "readmin"; + public string Description => "Re-admins you if you previously de-adminned."; + public string Help => "Usage: readmin"; + + public void Execute(IConsoleShell shell, IPlayerSession? player, string[] args) + { + if (player == null) + { + shell.SendText(player, "You cannot use this command from the server console."); + return; + } + + var mgr = IoCManager.Resolve(); + + if (mgr.GetAdminData(player, includeDeAdmin: true) == null) + { + shell.SendText(player, "You're not an admin."); + return; + } + + mgr.ReAdmin(player); + } + } +} diff --git a/Content.Server/Administration/ReadyAll.cs b/Content.Server/Administration/Commands/ReadyAll.cs similarity index 90% rename from Content.Server/Administration/ReadyAll.cs rename to Content.Server/Administration/Commands/ReadyAll.cs index 05d0b48fc8..ec703bb5b7 100644 --- a/Content.Server/Administration/ReadyAll.cs +++ b/Content.Server/Administration/Commands/ReadyAll.cs @@ -1,12 +1,14 @@ #nullable enable using Content.Server.GameTicking; using Content.Server.Interfaces.GameTicking; +using Content.Shared.Administration; using Robust.Server.Interfaces.Console; using Robust.Server.Interfaces.Player; using Robust.Shared.IoC; -namespace Content.Server.Administration +namespace Content.Server.Administration.Commands { + [AdminCommand(AdminFlags.Server)] public class ReadyAll : IClientCommand { public string Command => "readyall"; diff --git a/Content.Server/Administration/Rejuvenate.cs b/Content.Server/Administration/Commands/Rejuvenate.cs similarity index 93% rename from Content.Server/Administration/Rejuvenate.cs rename to Content.Server/Administration/Commands/Rejuvenate.cs index deb59d4260..69c79a36b8 100644 --- a/Content.Server/Administration/Rejuvenate.cs +++ b/Content.Server/Administration/Commands/Rejuvenate.cs @@ -1,4 +1,5 @@ using Content.Server.GlobalVerbs; +using Content.Shared.Administration; using Robust.Server.Interfaces.Console; using Robust.Server.Interfaces.Player; using Robust.Shared.GameObjects; @@ -6,8 +7,9 @@ using Robust.Shared.Interfaces.GameObjects; using Robust.Shared.IoC; using Robust.Shared.Localization; -namespace Content.Server.Administration +namespace Content.Server.Administration.Commands { + [AdminCommand(AdminFlags.Admin)] class Rejuvenate : IClientCommand { public string Command => "rejuvenate"; diff --git a/Content.Server/Administration/WarpCommand.cs b/Content.Server/Administration/Commands/WarpCommand.cs similarity index 97% rename from Content.Server/Administration/WarpCommand.cs rename to Content.Server/Administration/Commands/WarpCommand.cs index 45fb041a40..6e48b9637d 100644 --- a/Content.Server/Administration/WarpCommand.cs +++ b/Content.Server/Administration/Commands/WarpCommand.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using System.Linq; using Content.Server.GameObjects.Components.Markers; +using Content.Shared.Administration; using Robust.Server.Interfaces.Console; using Robust.Server.Interfaces.Player; using Robust.Shared.Enums; @@ -10,8 +11,9 @@ using Robust.Shared.Interfaces.Map; using Robust.Shared.IoC; using Robust.Shared.Map; -namespace Content.Server.Administration +namespace Content.Server.Administration.Commands { + [AdminCommand(AdminFlags.Admin)] public class WarpCommand : IClientCommand { public string Command => "warp"; diff --git a/Content.Server/Administration/IAdminManager.cs b/Content.Server/Administration/IAdminManager.cs new file mode 100644 index 0000000000..e475ebeec8 --- /dev/null +++ b/Content.Server/Administration/IAdminManager.cs @@ -0,0 +1,47 @@ +using System.Collections.Generic; +using Content.Shared.Administration; +using Robust.Server.Interfaces.Player; + +#nullable enable + +namespace Content.Server.Administration +{ + /// + /// Manages server administrators and their permission flags. + /// + public interface IAdminManager + { + /// + /// Gets all active admins currently on the server. + /// + /// + /// This does not include admins that are de-adminned. + /// + IEnumerable ActiveAdmins { get; } + + /// + /// Gets the admin data for a player, if they are an admin. + /// + /// The player to get admin data for. + /// + /// Whether to return admin data for admins that are current de-adminned. + /// + /// if the player is not an admin. + AdminData? GetAdminData(IPlayerSession session, bool includeDeAdmin = false); + + /// + /// De-admins an admin temporarily so they are effectively a normal player. + /// + /// + /// De-adminned admins are able to re-admin at any time if they so desire. + /// + void DeAdmin(IPlayerSession session); + + /// + /// Re-admins a de-adminned admin. + /// + void ReAdmin(IPlayerSession session); + + void Initialize(); + } +} diff --git a/Content.Server/Atmos/AtmosCommands.cs b/Content.Server/Atmos/AtmosCommands.cs index 3785e7fe85..476849febf 100644 --- a/Content.Server/Atmos/AtmosCommands.cs +++ b/Content.Server/Atmos/AtmosCommands.cs @@ -1,8 +1,10 @@ #nullable enable using System; +using Content.Server.Administration; using Content.Server.GameObjects.Components.Atmos; using Content.Server.GameObjects.EntitySystems; using Content.Server.GameObjects.EntitySystems.Atmos; +using Content.Shared.Administration; using Content.Shared.Atmos; using Robust.Server.Interfaces.Console; using Robust.Server.Interfaces.Player; @@ -15,6 +17,7 @@ using Robust.Shared.Maths; namespace Content.Server.Atmos { + [AdminCommand(AdminFlags.Debug)] public class AddAtmos : IClientCommand { public string Command => "addatmos"; diff --git a/Content.Server/Chat/ChatCommands.cs b/Content.Server/Chat/ChatCommands.cs index 94aa2ae25c..e2d32aa25c 100644 --- a/Content.Server/Chat/ChatCommands.cs +++ b/Content.Server/Chat/ChatCommands.cs @@ -1,5 +1,6 @@ using System; using System.Linq; +using Content.Server.Administration; using Content.Server.GameObjects.Components.GUI; using Content.Server.GameObjects.Components.Items.Storage; using Content.Server.GameObjects.Components.Observer; @@ -8,6 +9,7 @@ using Content.Server.Interfaces.GameObjects; using Content.Server.Observer; using Content.Server.Players; using Content.Server.Utility; +using Content.Shared.Administration; using Content.Shared.Damage; using Content.Shared.GameObjects.Components.Damage; using Content.Shared.Interfaces; @@ -20,6 +22,7 @@ using Robust.Shared.Localization; namespace Content.Server.Chat { + [AnyCommand] internal class SayCommand : IClientCommand { public string Command => "say"; @@ -51,6 +54,7 @@ namespace Content.Server.Chat } } + [AnyCommand] internal class MeCommand : IClientCommand { public string Command => "me"; @@ -76,6 +80,7 @@ namespace Content.Server.Chat } } + [AnyCommand] internal class OOCCommand : IClientCommand { public string Command => "ooc"; @@ -96,6 +101,7 @@ namespace Content.Server.Chat } } + [AdminCommand(AdminFlags.Admin)] internal class AdminChatCommand : IClientCommand { public string Command => "asay"; @@ -116,6 +122,7 @@ namespace Content.Server.Chat } } + [AnyCommand] internal class SuicideCommand : IClientCommand { public string Command => "suicide"; diff --git a/Content.Server/Chat/ChatManager.cs b/Content.Server/Chat/ChatManager.cs index 6550d58fb5..df2b087372 100644 --- a/Content.Server/Chat/ChatManager.cs +++ b/Content.Server/Chat/ChatManager.cs @@ -1,6 +1,6 @@ using System.Collections.Generic; using System.Linq; -using Content.Server.GameObjects.Components; +using Content.Server.Administration; using Content.Server.GameObjects.Components.GUI; using Content.Server.GameObjects.Components.Headset; using Content.Server.GameObjects.Components.Items.Storage; @@ -12,7 +12,6 @@ using Content.Shared.Chat; using Content.Shared.GameObjects.Components.Inventory; using Content.Shared.GameObjects.EntitySystems; using Content.Shared.Interfaces; -using Robust.Server.Console; using Robust.Server.Interfaces.GameObjects; using Robust.Server.Interfaces.Player; using Robust.Shared.GameObjects.Systems; @@ -47,7 +46,7 @@ namespace Content.Server.Chat [Dependency] private readonly IServerNetManager _netManager = default!; [Dependency] private readonly IPlayerManager _playerManager = default!; [Dependency] private readonly IMoMMILink _mommiLink = default!; - [Dependency] private readonly IConGroupController _conGroupController = default!; + [Dependency] private readonly IAdminManager _adminManager = default!; public void Initialize() { @@ -231,12 +230,7 @@ namespace Content.Server.Chat return; } - if (!_conGroupController.CanCommand(player, "asay")) - { - SendOOC(player, message); - return; - } - var clients = _playerManager.GetPlayersBy(x => _conGroupController.CanCommand(x, "asay")).Select(p => p.ConnectedClient);; + var clients = _adminManager.ActiveAdmins.Select(p => p.ConnectedClient); var msg = _netManager.CreateNetMessage(); diff --git a/Content.Server/Commands/AttachBodyPartCommand.cs b/Content.Server/Commands/AttachBodyPartCommand.cs index b19c829611..72128390b5 100644 --- a/Content.Server/Commands/AttachBodyPartCommand.cs +++ b/Content.Server/Commands/AttachBodyPartCommand.cs @@ -1,5 +1,7 @@ #nullable enable +using Content.Server.Administration; using Content.Server.GameObjects.Components.Body.Part; +using Content.Shared.Administration; using Content.Shared.GameObjects.Components.Body; using Content.Shared.GameObjects.Components.Body.Part; using Robust.Server.Interfaces.Console; @@ -10,6 +12,7 @@ using Robust.Shared.IoC; namespace Content.Server.Commands { + [AdminCommand(AdminFlags.Fun)] public class AttachBodyPartCommand : IClientCommand { public string Command => "attachbodypart"; diff --git a/Content.Server/Commands/HideContainedContextCommand.cs b/Content.Server/Commands/HideContainedContextCommand.cs index a4c7393146..ad975313a1 100644 --- a/Content.Server/Commands/HideContainedContextCommand.cs +++ b/Content.Server/Commands/HideContainedContextCommand.cs @@ -1,11 +1,14 @@ #nullable enable +using Content.Server.Administration; using Content.Server.GameObjects.EntitySystems; +using Content.Shared.Administration; using Robust.Server.Interfaces.Console; using Robust.Server.Interfaces.Player; using Robust.Shared.GameObjects.Systems; namespace Content.Server.Commands { + [AdminCommand(AdminFlags.Debug)] public class HideContainedContextCommand : IClientCommand { public string Command => "hidecontainedcontext"; diff --git a/Content.Server/Commands/ShowContainedContextCommand.cs b/Content.Server/Commands/ShowContainedContextCommand.cs index 3b37d6375e..8afcc5501b 100644 --- a/Content.Server/Commands/ShowContainedContextCommand.cs +++ b/Content.Server/Commands/ShowContainedContextCommand.cs @@ -1,11 +1,14 @@ #nullable enable +using Content.Server.Administration; using Content.Server.GameObjects.EntitySystems; +using Content.Shared.Administration; using Robust.Server.Interfaces.Console; using Robust.Server.Interfaces.Player; using Robust.Shared.GameObjects.Systems; namespace Content.Server.Commands { + [AdminCommand(AdminFlags.Debug)] public class ShowContainedContextCommand : IClientCommand { public const string CommandName = "showcontainedcontext"; diff --git a/Content.Server/Database/ServerDbBase.cs b/Content.Server/Database/ServerDbBase.cs index dbc1cc1231..7c0a13af63 100644 --- a/Content.Server/Database/ServerDbBase.cs +++ b/Content.Server/Database/ServerDbBase.cs @@ -217,6 +217,15 @@ namespace Content.Server.Database */ public abstract Task AddConnectionLogAsync(NetUserId userId, string userName, IPAddress address); + /* + * ADMIN STUFF + */ + public async Task GetAdminDataForAsync(NetUserId userId) + { + await using var db = await GetDb(); + + return await db.DbContext.Admin.SingleOrDefaultAsync(p => p.UserId == userId.UserId); + } protected abstract Task GetDb(); diff --git a/Content.Server/Database/ServerDbManager.cs b/Content.Server/Database/ServerDbManager.cs index 8d7dc816c4..f7eb67ee09 100644 --- a/Content.Server/Database/ServerDbManager.cs +++ b/Content.Server/Database/ServerDbManager.cs @@ -45,6 +45,9 @@ namespace Content.Server.Database // Connection log Task AddConnectionLogAsync(NetUserId userId, string userName, IPAddress address); + + // Admins + Task GetAdminDataForAsync(NetUserId userId); } public sealed class ServerDbManager : IServerDbManager @@ -137,6 +140,11 @@ namespace Content.Server.Database return _db.AddConnectionLogAsync(userId, userName, address); } + public Task GetAdminDataForAsync(NetUserId userId) + { + return _db.GetAdminDataForAsync(userId); + } + private DbContextOptions CreatePostgresOptions() { var host = _cfg.GetCVar(CCVars.DatabasePgHost); diff --git a/Content.Server/EntryPoint.cs b/Content.Server/EntryPoint.cs index 9542e2b36a..3bc16dee3a 100644 --- a/Content.Server/EntryPoint.cs +++ b/Content.Server/EntryPoint.cs @@ -1,4 +1,5 @@ -using Content.Server.AI.Utility.Considerations; +using Content.Server.Administration; +using Content.Server.AI.Utility.Considerations; using Content.Server.AI.WorldState; using Content.Server.Database; using Content.Server.GameObjects.Components.Mobs.Speech; @@ -77,6 +78,7 @@ namespace Content.Server IoCManager.Resolve().Initialize(); IoCManager.Resolve().Initialize(); IoCManager.Resolve().Initialize(); + IoCManager.Resolve().Initialize(); } public override void Update(ModUpdateLevel level, FrameEventArgs frameEventArgs) diff --git a/Content.Server/GameObjects/Components/Body/BodyCommands.cs b/Content.Server/GameObjects/Components/Body/BodyCommands.cs index 867ad77f9f..d0129b7f1a 100644 --- a/Content.Server/GameObjects/Components/Body/BodyCommands.cs +++ b/Content.Server/GameObjects/Components/Body/BodyCommands.cs @@ -1,6 +1,8 @@ #nullable enable using System; using System.Linq; +using Content.Server.Administration; +using Content.Shared.Administration; using Content.Shared.Damage; using Content.Shared.GameObjects.Components.Body; using Content.Shared.GameObjects.Components.Body.Part; @@ -16,6 +18,7 @@ using Robust.Shared.Random; namespace Content.Server.GameObjects.Components.Body { + [AdminCommand(AdminFlags.Fun)] class AddHandCommand : IClientCommand { public const string DefaultHandPrototype = "LeftHandHuman"; @@ -149,6 +152,7 @@ namespace Content.Server.GameObjects.Components.Body } } + [AdminCommand(AdminFlags.Fun)] class RemoveHandCommand : IClientCommand { public string Command => "removehand"; @@ -190,6 +194,7 @@ namespace Content.Server.GameObjects.Components.Body } } + [AdminCommand(AdminFlags.Fun)] class DestroyMechanismCommand : IClientCommand { public string Command => "destroymechanism"; @@ -242,6 +247,7 @@ namespace Content.Server.GameObjects.Components.Body } } + [AdminCommand(AdminFlags.Fun)] class HurtCommand : IClientCommand { public string Command => "hurt"; diff --git a/Content.Server/GameObjects/Components/Damage/DamageCommands.cs b/Content.Server/GameObjects/Components/Damage/DamageCommands.cs index d691892293..e90fc5d294 100644 --- a/Content.Server/GameObjects/Components/Damage/DamageCommands.cs +++ b/Content.Server/GameObjects/Components/Damage/DamageCommands.cs @@ -1,7 +1,9 @@ #nullable enable using System; using System.Diagnostics.CodeAnalysis; +using Content.Server.Administration; using Content.Server.GameObjects.Components.Atmos; +using Content.Shared.Administration; using Content.Shared.GameObjects.Components.Damage; using Robust.Server.Interfaces.Console; using Robust.Server.Interfaces.Player; @@ -115,6 +117,7 @@ namespace Content.Server.GameObjects.Components.Damage } } + [AdminCommand(AdminFlags.Fun)] public class AddDamageFlagCommand : DamageFlagCommand { public override string Command => "adddamageflag"; @@ -133,6 +136,7 @@ namespace Content.Server.GameObjects.Components.Damage } } + [AdminCommand(AdminFlags.Fun)] public class RemoveDamageFlagCommand : DamageFlagCommand { public override string Command => "removedamageflag"; @@ -151,6 +155,7 @@ namespace Content.Server.GameObjects.Components.Damage } } + [AdminCommand(AdminFlags.Admin)] public class GodModeCommand : IClientCommand { public string Command => "godmode"; diff --git a/Content.Server/GameObjects/Components/Disposal/DisposalCommands.cs b/Content.Server/GameObjects/Components/Disposal/DisposalCommands.cs index 238b31c737..2f549a8037 100644 --- a/Content.Server/GameObjects/Components/Disposal/DisposalCommands.cs +++ b/Content.Server/GameObjects/Components/Disposal/DisposalCommands.cs @@ -1,4 +1,6 @@ #nullable enable +using Content.Server.Administration; +using Content.Shared.Administration; using Robust.Server.Interfaces.Console; using Robust.Server.Interfaces.Player; using Robust.Shared.GameObjects; @@ -8,6 +10,7 @@ using Robust.Shared.Localization; namespace Content.Server.GameObjects.Components.Disposal { + [AdminCommand(AdminFlags.Debug)] public class TubeConnectionsCommand : IClientCommand { public string Command => "tubeconnections"; diff --git a/Content.Server/GameObjects/Components/Interactable/ToolCommands.cs b/Content.Server/GameObjects/Components/Interactable/ToolCommands.cs index c4ec4a2a05..18faf899b6 100644 --- a/Content.Server/GameObjects/Components/Interactable/ToolCommands.cs +++ b/Content.Server/GameObjects/Components/Interactable/ToolCommands.cs @@ -1,5 +1,7 @@ #nullable enable using System.Linq; +using Content.Server.Administration; +using Content.Shared.Administration; using Content.Shared.Maps; using JetBrains.Annotations; using Robust.Server.Interfaces.Console; @@ -14,7 +16,7 @@ namespace Content.Server.GameObjects.Components.Interactable /// /// /// - [UsedImplicitly] + [AdminCommand(AdminFlags.Debug)] class TilePryCommand : IClientCommand { public string Command => "tilepry"; @@ -69,7 +71,7 @@ namespace Content.Server.GameObjects.Components.Interactable } } - [UsedImplicitly] + [AdminCommand(AdminFlags.Debug)] class AnchorCommand : IClientCommand { public string Command => "anchor"; @@ -114,7 +116,7 @@ namespace Content.Server.GameObjects.Components.Interactable } } - [UsedImplicitly] + [AdminCommand(AdminFlags.Debug)] class UnAnchorCommand : IClientCommand { public string Command => "unanchor"; diff --git a/Content.Server/GameObjects/Components/Mobs/Speech/SpeechComponent.cs b/Content.Server/GameObjects/Components/Mobs/Speech/SpeechComponent.cs index 1140770e9d..3af74dbaa0 100644 --- a/Content.Server/GameObjects/Components/Mobs/Speech/SpeechComponent.cs +++ b/Content.Server/GameObjects/Components/Mobs/Speech/SpeechComponent.cs @@ -1,5 +1,7 @@ using System; using System.Linq; +using Content.Server.Administration; +using Content.Shared.Administration; using Robust.Server.Interfaces.Console; using Robust.Server.Interfaces.Player; using Robust.Shared.Interfaces.GameObjects; @@ -17,6 +19,7 @@ namespace Content.Server.GameObjects.Components.Mobs.Speech public string Accentuate(string message); } + [AdminCommand(AdminFlags.Fun)] public class AddAccent : IClientCommand { public string Command => "addaccent"; @@ -38,12 +41,12 @@ namespace Content.Server.GameObjects.Components.Mobs.Speech shell.SendText(player, "You don't have a player!"); return; } - + var compFactory = IoCManager.Resolve(); - + if (args[0] == "?") { - // Get all components that implement the ISpeechComponent except + // Get all components that implement the ISpeechComponent except var speeches = compFactory.GetAllRefTypes() .Where(c => typeof(IAccentComponent).IsAssignableFrom(c) && c.IsClass); var msg = ""; diff --git a/Content.Server/GameObjects/Components/Observer/GhostComponent.cs b/Content.Server/GameObjects/Components/Observer/GhostComponent.cs index 1a36f8fd8c..8fd45b1bdc 100644 --- a/Content.Server/GameObjects/Components/Observer/GhostComponent.cs +++ b/Content.Server/GameObjects/Components/Observer/GhostComponent.cs @@ -102,7 +102,7 @@ namespace Content.Server.GameObjects.Components.Observer { if (player.AttachedEntity != null && warp.PlayerTarget == player.AttachedEntity.Uid) { - session?.AttachedEntity!.Transform.Coordinates = + session!.AttachedEntity!.Transform.Coordinates = player.AttachedEntity.Transform.Coordinates; } } @@ -113,7 +113,7 @@ namespace Content.Server.GameObjects.Components.Observer { if (warp.WarpName == warpPoint.Location) { - session?.AttachedEntity!.Transform.Coordinates = warpPoint.Owner.Transform.Coordinates ; + session!.AttachedEntity!.Transform.Coordinates = warpPoint.Owner.Transform.Coordinates ; } } } diff --git a/Content.Server/GameObjects/EntitySystems/AI/AiFactionTagSystem.cs b/Content.Server/GameObjects/EntitySystems/AI/AiFactionTagSystem.cs index e12fc1633e..ea35dceb99 100644 --- a/Content.Server/GameObjects/EntitySystems/AI/AiFactionTagSystem.cs +++ b/Content.Server/GameObjects/EntitySystems/AI/AiFactionTagSystem.cs @@ -1,7 +1,9 @@ using System; using System.Collections.Generic; using System.Text; +using Content.Server.Administration; using Content.Server.GameObjects.Components.AI; +using Content.Shared.Administration; using Robust.Server.Interfaces.Console; using Robust.Server.Interfaces.Player; using Robust.Shared.GameObjects.Systems; @@ -19,12 +21,12 @@ namespace Content.Server.GameObjects.EntitySystems.AI * Currently factions are implicitly friendly if they are not hostile. * This may change where specified friendly factions are listed. (e.g. to get number of friendlies in area). */ - + public Faction GetHostileFactions(Faction faction) => _hostileFactions.TryGetValue(faction, out var hostiles) ? hostiles : Faction.None; - + private Dictionary _hostileFactions = new Dictionary { - {Faction.NanoTransen, + {Faction.NanoTransen, Faction.SimpleHostile | Faction.Syndicate | Faction.Xeno}, {Faction.SimpleHostile, Faction.NanoTransen | Faction.Syndicate @@ -35,11 +37,11 @@ namespace Content.Server.GameObjects.EntitySystems.AI }, {Faction.Syndicate, Faction.NanoTransen | Faction.SimpleHostile | Faction.Xeno}, - {Faction.Xeno, + {Faction.Xeno, Faction.NanoTransen | Faction.Syndicate}, }; - public Faction GetFactions(IEntity entity) => + public Faction GetFactions(IEntity entity) => entity.TryGetComponent(out AiFactionTagComponent factionTags) ? factionTags.Factions : Faction.None; @@ -76,7 +78,7 @@ namespace Content.Server.GameObjects.EntitySystems.AI hostileFactions &= ~target; _hostileFactions[source] = hostileFactions; } - + public void MakeHostile(Faction source, Faction target) { if (!_hostileFactions.TryGetValue(source, out var hostileFactions)) @@ -89,12 +91,13 @@ namespace Content.Server.GameObjects.EntitySystems.AI _hostileFactions[source] = hostileFactions; } } - + + [AdminCommand(AdminFlags.Fun)] public sealed class FactionCommand : IClientCommand { public string Command => "factions"; public string Description => "Update / list factional relationships for NPCs."; - public string Help => "faction target\n" + + public string Help => "faction target\n" + "faction list: hostile factions"; public void Execute(IConsoleShell shell, IPlayerSession player, string[] args) @@ -108,11 +111,11 @@ namespace Content.Server.GameObjects.EntitySystems.AI continue; result.Append(value + "\n"); } - + shell.SendText(player, result.ToString()); return; } - + if (args.Length < 2) { shell.SendText(player, Loc.GetString("Need more args")); @@ -141,7 +144,7 @@ namespace Content.Server.GameObjects.EntitySystems.AI shell.SendText(player, Loc.GetString("Invalid target faction")); return; } - + EntitySystem.Get().MakeFriendly(faction, targetFaction); shell.SendText(player, Loc.GetString("Command successful")); break; @@ -157,7 +160,7 @@ namespace Content.Server.GameObjects.EntitySystems.AI shell.SendText(player, Loc.GetString("Invalid target faction")); return; } - + EntitySystem.Get().MakeHostile(faction, targetFaction); shell.SendText(player, Loc.GetString("Command successful")); break; @@ -172,4 +175,4 @@ namespace Content.Server.GameObjects.EntitySystems.AI return; } } -} \ No newline at end of file +} diff --git a/Content.Server/GameObjects/EntitySystems/AI/AiSystem.cs b/Content.Server/GameObjects/EntitySystems/AI/AiSystem.cs index f951020547..abb8876ab3 100644 --- a/Content.Server/GameObjects/EntitySystems/AI/AiSystem.cs +++ b/Content.Server/GameObjects/EntitySystems/AI/AiSystem.cs @@ -2,7 +2,9 @@ using System; using System.Collections.Generic; using System.Linq; +using Content.Server.Administration; using Content.Server.GameObjects.Components.Movement; +using Content.Shared.Administration; using Content.Shared.GameObjects.Components.Movement; using JetBrains.Annotations; using Robust.Server.AI; @@ -75,7 +77,7 @@ namespace Content.Server.GameObjects.EntitySystems.AI break; case false: _awakeAi.Add(message.Processor); - + if (_awakeAi.Count > cvarMaxUpdates) { Logger.Warning($"AI limit exceeded: {_awakeAi.Count} / {cvarMaxUpdates}"); @@ -101,7 +103,7 @@ namespace Content.Server.GameObjects.EntitySystems.AI toRemove.Add(processor); continue; } - + processor.Update(frameTime); count++; } @@ -145,6 +147,7 @@ namespace Content.Server.GameObjects.EntitySystems.AI public bool ProcessorTypeExists(string name) => _processorTypes.ContainsKey(name); + [AdminCommand(AdminFlags.Fun)] private class AddAiCommand : IClientCommand { public string Command => "addai"; diff --git a/Content.Server/GameObjects/EntitySystems/SignalLinkerSystem.cs b/Content.Server/GameObjects/EntitySystems/SignalLinkerSystem.cs index a9c58b076a..88782c51a4 100644 --- a/Content.Server/GameObjects/EntitySystems/SignalLinkerSystem.cs +++ b/Content.Server/GameObjects/EntitySystems/SignalLinkerSystem.cs @@ -1,5 +1,7 @@ using System.Collections.Generic; +using Content.Server.Administration; using Content.Server.GameObjects.Components.MachineLinking; +using Content.Shared.Administration; using Robust.Server.Interfaces.Console; using Robust.Server.Interfaces.Player; using Robust.Shared.GameObjects; @@ -96,6 +98,7 @@ namespace Content.Server.GameObjects.EntitySystems } + [AdminCommand(AdminFlags.Debug)] public class SignalLinkerCommand : IClientCommand { public string Command => "signallink"; diff --git a/Content.Server/GameTicking/GameTicker.cs b/Content.Server/GameTicking/GameTicker.cs index b6d396e95e..6b83a46088 100644 --- a/Content.Server/GameTicking/GameTicker.cs +++ b/Content.Server/GameTicking/GameTicker.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using System.Threading; +using Content.Server.Administration; using Content.Server.GameObjects.Components.Access; using Content.Server.GameObjects.Components.GUI; using Content.Server.GameObjects.Components.Items.Storage; diff --git a/Content.Server/GameTicking/GameTickerCommands.cs b/Content.Server/GameTicking/GameTickerCommands.cs index c84e448785..6fa0f2641a 100644 --- a/Content.Server/GameTicking/GameTickerCommands.cs +++ b/Content.Server/GameTicking/GameTickerCommands.cs @@ -1,8 +1,10 @@ using System; using System.Collections.Generic; using System.Linq; +using Content.Server.Administration; using Content.Server.Interfaces.GameTicking; using Content.Server.Players; +using Content.Shared.Administration; using Content.Shared.Maps; using Content.Shared.Roles; using Robust.Server.Interfaces.Console; @@ -18,6 +20,7 @@ using Robust.Shared.Utility; namespace Content.Server.GameTicking { + [AdminCommand(AdminFlags.Server)] class DelayStartCommand : IClientCommand { public string Command => "delaystart"; @@ -60,6 +63,7 @@ namespace Content.Server.GameTicking } } + [AdminCommand(AdminFlags.Server)] class StartRoundCommand : IClientCommand { public string Command => "startround"; @@ -80,6 +84,7 @@ namespace Content.Server.GameTicking } } + [AdminCommand(AdminFlags.Server)] class EndRoundCommand : IClientCommand { public string Command => "endround"; @@ -100,6 +105,7 @@ namespace Content.Server.GameTicking } } + [AdminCommand(AdminFlags.Server)] public class NewRoundCommand : IClientCommand { public string Command => "restartround"; @@ -165,6 +171,7 @@ namespace Content.Server.GameTicking } } + [AnyCommand] class ObserveCommand : IClientCommand { public string Command => "observe"; @@ -183,6 +190,7 @@ namespace Content.Server.GameTicking } } + [AnyCommand] class JoinGameCommand : IClientCommand { [Dependency] private readonly IPrototypeManager _prototypeManager = default!; @@ -228,6 +236,7 @@ namespace Content.Server.GameTicking } } + [AnyCommand] class ToggleReadyCommand : IClientCommand { public string Command => "toggleready"; @@ -246,6 +255,7 @@ namespace Content.Server.GameTicking } } + [AdminCommand(AdminFlags.Server)] class ToggleDisallowLateJoinCommand: IClientCommand { public string Command => "toggledisallowlatejoin"; @@ -274,6 +284,7 @@ namespace Content.Server.GameTicking } } + [AdminCommand(AdminFlags.Server)] class SetGamePresetCommand : IClientCommand { public string Command => "setgamepreset"; @@ -294,6 +305,7 @@ namespace Content.Server.GameTicking } } + [AdminCommand(AdminFlags.Server)] class ForcePresetCommand : IClientCommand { public string Command => "forcepreset"; @@ -327,6 +339,7 @@ namespace Content.Server.GameTicking } } + [AdminCommand(AdminFlags.Server | AdminFlags.Mapping)] class MappingCommand : IClientCommand { public string Command => "mapping"; @@ -383,6 +396,7 @@ namespace Content.Server.GameTicking } } + [AdminCommand(AdminFlags.Mapping)] class TileWallsCommand : IClientCommand { // ReSharper disable once StringLiteralTypo diff --git a/Content.Server/Mobs/Commands.cs b/Content.Server/Mobs/Commands.cs index 6d64578411..45af6e11b6 100644 --- a/Content.Server/Mobs/Commands.cs +++ b/Content.Server/Mobs/Commands.cs @@ -1,7 +1,9 @@ using System.Text; +using Content.Server.Administration; using Content.Server.GameObjects.Components.Mobs; using Content.Server.Mobs.Roles; using Content.Server.Players; +using Content.Shared.Administration; using Content.Shared.Roles; using Robust.Server.Interfaces.Console; using Robust.Server.Interfaces.Player; @@ -12,6 +14,7 @@ using Robust.Shared.Prototypes; namespace Content.Server.Mobs { + [AdminCommand(AdminFlags.Admin)] public class MindInfoCommand : IClientCommand { public string Command => "mindinfo"; @@ -49,6 +52,7 @@ namespace Content.Server.Mobs } } + [AdminCommand(AdminFlags.Fun)] public class AddRoleCommand : IClientCommand { [Dependency] private readonly IPrototypeManager _prototypeManager = default!; @@ -81,6 +85,7 @@ namespace Content.Server.Mobs } } + [AdminCommand(AdminFlags.Fun)] public class RemoveRoleCommand : IClientCommand { [Dependency] private readonly IPrototypeManager _prototypeManager = default!; @@ -113,6 +118,7 @@ namespace Content.Server.Mobs } } + [AdminCommand(AdminFlags.Debug)] public class AddOverlayCommand : IClientCommand { public string Command => "addoverlay"; @@ -137,6 +143,7 @@ namespace Content.Server.Mobs } } + [AdminCommand(AdminFlags.Debug)] public class RemoveOverlayCommand : IClientCommand { public string Command => "rmoverlay"; diff --git a/Content.Server/Observer/Ghost.cs b/Content.Server/Observer/Ghost.cs index 8e97025f66..6aca5cff89 100644 --- a/Content.Server/Observer/Ghost.cs +++ b/Content.Server/Observer/Ghost.cs @@ -1,3 +1,4 @@ +using Content.Server.Administration; using Content.Server.GameObjects.Components.Mobs; using Content.Server.GameObjects.Components.Observer; using Content.Server.Interfaces.GameTicking; @@ -12,6 +13,7 @@ using Robust.Shared.IoC; namespace Content.Server.Observer { + [AnyCommand] public class Ghost : IClientCommand { public string Command => "ghost"; diff --git a/Content.Server/Players/PlayerData.cs b/Content.Server/Players/PlayerData.cs index c66b6f7024..d398d87747 100644 --- a/Content.Server/Players/PlayerData.cs +++ b/Content.Server/Players/PlayerData.cs @@ -25,6 +25,12 @@ namespace Content.Server.Players [ViewVariables] public Mind? Mind { get; set; } + /// + /// If true, the player is an admin and they explicitly de-adminned mid-game, + /// so they should not regain admin if they reconnect. + /// + public bool ExplicitlyDeadminned { get; set; } + public void WipeMind() { Mind?.ChangeOwningPlayer(null); diff --git a/Content.Server/ServerContentIoC.cs b/Content.Server/ServerContentIoC.cs index dadb213851..abde376c85 100644 --- a/Content.Server/ServerContentIoC.cs +++ b/Content.Server/ServerContentIoC.cs @@ -1,4 +1,5 @@ -using Content.Server.AI.Utility.Considerations; +using Content.Server.Administration; +using Content.Server.AI.Utility.Considerations; using Content.Server.AI.WorldState; using Content.Server.Cargo; using Content.Server.Chat; @@ -44,6 +45,7 @@ namespace Content.Server IoCManager.Register(); IoCManager.Register(); IoCManager.Register(); + IoCManager.Register(); } } } diff --git a/Content.Server/ServerNotifyManager.cs b/Content.Server/ServerNotifyManager.cs index 7dd9984245..aac886c0aa 100644 --- a/Content.Server/ServerNotifyManager.cs +++ b/Content.Server/ServerNotifyManager.cs @@ -1,5 +1,7 @@ +using Content.Server.Administration; using Content.Server.Interfaces; using Content.Shared; +using Content.Shared.Administration; using Content.Shared.Interfaces; using Robust.Server.Interfaces.Console; using Robust.Server.Interfaces.GameObjects; @@ -71,6 +73,7 @@ namespace Content.Server _netManager.ServerSendMessage(netMessage, actor.playerSession.ConnectedClient); } + [AdminCommand(AdminFlags.Debug)] public class PopupMsgCommand : IClientCommand { public string Command => "srvpopupmsg"; diff --git a/Content.Server/StationEvents/StationEventCommand.cs b/Content.Server/StationEvents/StationEventCommand.cs index 108baba460..2ec3587142 100644 --- a/Content.Server/StationEvents/StationEventCommand.cs +++ b/Content.Server/StationEvents/StationEventCommand.cs @@ -1,6 +1,7 @@ #nullable enable +using Content.Server.Administration; using Content.Server.GameObjects.EntitySystems.StationEvents; -using JetBrains.Annotations; +using Content.Shared.Administration; using Robust.Server.Interfaces.Console; using Robust.Server.Interfaces.Player; using Robust.Shared.GameObjects.Systems; @@ -8,14 +9,14 @@ using Robust.Shared.Localization; namespace Content.Server.StationEvents { - [UsedImplicitly] + [AdminCommand(AdminFlags.Server)] public sealed class StationEventCommand : IClientCommand { public string Command => "events"; public string Description => "Provides admin control to station events"; - public string Help => "events >\n" + + public string Help => "events >\n" + "list: return all event names that can be run\n " + - "pause: stop all random events from running\n" + + "pause: stop all random events from running\n" + "resume: allow random events to run again\n" + "run: start a particular event now; is case-insensitive and not localized"; public void Execute(IConsoleShell shell, IPlayerSession? player, string[] args) @@ -25,19 +26,19 @@ namespace Content.Server.StationEvents shell.SendText(player, "Need more args"); return; } - + if (args[0] == "list") { var resultText = "Random\n" + EntitySystem.Get().GetEventNames(); shell.SendText(player, resultText); return; } - + // Didn't use a "toggle" so it's explicit if (args[0] == "pause") { var stationEventSystem = EntitySystem.Get(); - + if (!stationEventSystem.Enabled) { shell.SendText(player, Loc.GetString("Station events are already paused")); @@ -50,11 +51,11 @@ namespace Content.Server.StationEvents return; } } - + if (args[0] == "resume") { var stationEventSystem = EntitySystem.Get(); - + if (stationEventSystem.Enabled) { shell.SendText(player, Loc.GetString("Station events are already running")); @@ -88,7 +89,7 @@ namespace Content.Server.StationEvents { resultText = EntitySystem.Get().RunEvent(eventName); } - + shell.SendText(player, resultText); return; } diff --git a/Content.Shared/Administration/AdminData.cs b/Content.Shared/Administration/AdminData.cs new file mode 100644 index 0000000000..70ff7c6876 --- /dev/null +++ b/Content.Shared/Administration/AdminData.cs @@ -0,0 +1,39 @@ +#nullable enable + +namespace Content.Shared.Administration +{ + public sealed class AdminData + { + public const string DefaultTitle = "Admin"; + + // Can be false if they're de-adminned with the ability to re-admin. + public bool Active; + public string? Title; + public AdminFlags Flags; + + public bool HasFlag(AdminFlags flag) + { + return Active && (Flags & flag) == flag; + } + + public bool CanViewVar() + { + return HasFlag(AdminFlags.VarEdit); + } + + public bool CanAdminPlace() + { + return HasFlag(AdminFlags.Spawn); + } + + public bool CanScript() + { + return HasFlag(AdminFlags.Host); + } + + public bool CanAdminMenu() + { + return HasFlag(AdminFlags.Admin); + } + } +} diff --git a/Content.Shared/Administration/AdminFlags.cs b/Content.Shared/Administration/AdminFlags.cs new file mode 100644 index 0000000000..4b3a856c55 --- /dev/null +++ b/Content.Shared/Administration/AdminFlags.cs @@ -0,0 +1,68 @@ +using System; + +namespace Content.Shared.Administration +{ + /// + /// Permissions that admins can have. + /// + [Flags] + public enum AdminFlags : uint + { + None = 0, + + /// + /// Basic admin verbs. + /// + Admin = 1 << 0, + + /// + /// Ability to ban people. + /// + Ban = 1 << 1, + + /// + /// Debug commands for coders. + /// + Debug = 1 << 2, + + /// + /// !!FUN!! + /// + Fun = 1 << 3, + + /// + /// Ability to edit permissions for other administrators. + /// + Permissions = 1 << 4, + + /// + /// Ability to control teh server like restart it or change the round type. + /// + Server = 1 << 5, + + /// + /// Ability to spawn stuff in. + /// + Spawn = 1 << 6, + + /// + /// Ability to use VV. + /// + VarEdit = 1 << 7, + + /// + /// Large mapping operations. + /// + Mapping = 1 << 8, + + /// + /// Makes you british. + /// + Piss = 1 << 9, + + /// + /// Dangerous host permissions like scsi. + /// + Host = 1u << 31, + } +} diff --git a/Content.Shared/Administration/AdminFlagsExt.cs b/Content.Shared/Administration/AdminFlagsExt.cs new file mode 100644 index 0000000000..abe198a1f0 --- /dev/null +++ b/Content.Shared/Administration/AdminFlagsExt.cs @@ -0,0 +1,73 @@ +using System; +using System.Collections.Generic; +using System.Numerics; + +namespace Content.Shared.Administration +{ + public static class AdminFlagsExt + { + private static readonly Dictionary NameFlagsMap = new Dictionary(); + private static readonly string[] FlagsNameMap = new string[32]; + + public static readonly AdminFlags Everything; + + static AdminFlagsExt() + { + var t = typeof(AdminFlags); + var flags = (AdminFlags[]) Enum.GetValues(t); + + foreach (var value in flags) + { + var name = value.ToString().ToUpper(); + + if (BitOperations.PopCount((uint) value) != 1) + { + continue; + } + + Everything |= value; + NameFlagsMap.Add(name, value); + FlagsNameMap[BitOperations.Log2((uint) value)] = name; + } + } + + public static AdminFlags NamesToFlags(IEnumerable names) + { + var flags = AdminFlags.None; + foreach (var name in names) + { + if (!NameFlagsMap.TryGetValue(name, out var value)) + { + throw new ArgumentException($"Invalid admin flag name: {name}"); + } + + flags |= value; + } + + return flags; + } + + public static AdminFlags NameToFlag(string name) + { + return NameFlagsMap[name]; + } + + public static string[] FlagsToNames(AdminFlags flags) + { + var array = new string[BitOperations.PopCount((uint) flags)]; + var highest = BitOperations.LeadingZeroCount((uint) flags); + + var ai = 0; + for (var i = 0; i < 32 - highest; i++) + { + var flagValue = (AdminFlags) (1u << i); + if ((flags & flagValue) != 0) + { + array[ai++] = FlagsNameMap[i]; + } + } + + return array; + } + } +} diff --git a/Content.Shared/CCVars.cs b/Content.Shared/CCVars.cs index d01c1d9c19..02d58f26bd 100644 --- a/Content.Shared/CCVars.cs +++ b/Content.Shared/CCVars.cs @@ -34,6 +34,9 @@ namespace Content.Shared public static readonly CVarDef GamePersistGuests = CVarDef.Create("game.persistguests", true, CVar.ARCHIVE | CVar.SERVERONLY); + public static readonly CVarDef + ConsoleLoginLocal = CVarDef.Create("console.loginlocal", true, CVar.ARCHIVE); + /* * Database stuff diff --git a/Content.Shared/Network/NetMessages/MsgUpdateAdminStatus.cs b/Content.Shared/Network/NetMessages/MsgUpdateAdminStatus.cs new file mode 100644 index 0000000000..bdeb4be645 --- /dev/null +++ b/Content.Shared/Network/NetMessages/MsgUpdateAdminStatus.cs @@ -0,0 +1,73 @@ +using Content.Shared.Administration; +using Lidgren.Network; +using Robust.Shared.Interfaces.Network; +using Robust.Shared.Network; + +namespace Content.Shared.Network.NetMessages +{ + public sealed class MsgUpdateAdminStatus : NetMessage + { + #region REQUIRED + + public const MsgGroups GROUP = MsgGroups.Command; + public const string NAME = nameof(MsgUpdateAdminStatus); + + public MsgUpdateAdminStatus(INetChannel channel) : base(NAME, GROUP) { } + + #endregion + + public AdminData Admin; + public string[] AvailableCommands; + + public override void ReadFromBuffer(NetIncomingMessage buffer) + { + var count = buffer.ReadVariableInt32(); + + AvailableCommands = new string[count]; + + for (var i = 0; i < count; i++) + { + AvailableCommands[i] = buffer.ReadString(); + } + + if (buffer.ReadBoolean()) + { + var active = buffer.ReadBoolean(); + buffer.ReadPadBits(); + var flags = (AdminFlags) buffer.ReadUInt32(); + var title = buffer.ReadString(); + + Admin = new AdminData + { + Active = active, + Title = title, + Flags = flags, + }; + } + + } + + public override void WriteToBuffer(NetOutgoingMessage buffer) + { + buffer.WriteVariableInt32(AvailableCommands.Length); + + foreach (var cmd in AvailableCommands) + { + buffer.Write(cmd); + } + + var isAdmin = Admin != null; + buffer.Write(isAdmin); + + if (isAdmin) + { + buffer.Write(Admin.Active); + buffer.WritePadBits(); + buffer.Write((uint) Admin.Flags); + buffer.Write(Admin.Title); + } + } + + public override NetDeliveryMethod DeliveryMethod => NetDeliveryMethod.ReliableOrdered; + } +} diff --git a/Content.Tests/Shared/Administration/AdminFlagsExtTest.cs b/Content.Tests/Shared/Administration/AdminFlagsExtTest.cs new file mode 100644 index 0000000000..e44a8a1719 --- /dev/null +++ b/Content.Tests/Shared/Administration/AdminFlagsExtTest.cs @@ -0,0 +1,35 @@ +using System; +using Content.Shared.Administration; +using NUnit.Framework; + +namespace Content.Tests.Shared.Administration +{ + [TestFixture] + [Parallelizable(ParallelScope.All)] + public class AdminFlagsExtTest + { + [Test] + [TestCase("ADMIN", AdminFlags.Admin)] + [TestCase("ADMIN,DEBUG", AdminFlags.Admin | AdminFlags.Debug)] + [TestCase("ADMIN,DEBUG,HOST", AdminFlags.Admin | AdminFlags.Debug | AdminFlags.Host)] + [TestCase("", AdminFlags.None)] + public void TestNamesToFlags(string namesConcat, AdminFlags flags) + { + var names = namesConcat.Split(",", StringSplitOptions.RemoveEmptyEntries); + + Assert.That(AdminFlagsExt.NamesToFlags(names), Is.EqualTo(flags)); + } + + [Test] + [TestCase("ADMIN", AdminFlags.Admin)] + [TestCase("ADMIN,DEBUG", AdminFlags.Admin | AdminFlags.Debug)] + [TestCase("ADMIN,DEBUG,HOST", AdminFlags.Admin | AdminFlags.Debug | AdminFlags.Host)] + [TestCase("", AdminFlags.None)] + public void TestFlagsToNames(string namesConcat, AdminFlags flags) + { + var names = namesConcat.Split(",", StringSplitOptions.RemoveEmptyEntries); + + Assert.That(AdminFlagsExt.FlagsToNames(flags), Is.EquivalentTo(names)); + } + } +} diff --git a/Resources/Groups/groups.yml b/Resources/Groups/groups.yml deleted file mode 100644 index 916f74d526..0000000000 --- a/Resources/Groups/groups.yml +++ /dev/null @@ -1,233 +0,0 @@ -- Index: 1 - Name: Player - Commands: - - login - - joingame - - help - - list - - say - - whisper - - me - - ooc - - observe - - toggleready - - ghost - - suicide - - hostlogin - -- Index: 50 - Name: Moderator - Commands: - - login - - logout - - joingame - - help - - list - - say - - whisper - - me - - ooc - - showtime - - observe - - toggleready - - ghost - - suicide - - kick - - listplayers - - loc - - hostlogin - - events - - factions - CanAdminMenu: true - -- Index: 100 - Name: Administrator - Commands: - - logout - - joingame - - help - - list - - say - - whisper - - me - - ooc - - showtime - - aghost - - observe - - toggleready - - ghost - - suicide - - spawn - - delete - - tp - - tpgrid - - setgamepreset - - forcepreset - - delaystart - - startround - - endround - - restartround - - respawn - - rejuvenate - - addcomp - - rmcomp - - controlmob - - kick - - listplayers - - loc - - lsmap - - lsgrid - - mindinfo - - addrole - - rmrole - - addoverlay - - rmoverlay - - showtime - - group - - addai - - warp - - hostlogin - - deleteewc - - asay - - mapping - - addhand - - removehand - - tilepry - - anchor - - unanchor - - tubeconnections - - tilewalls - - events - - destroymechanism - - addaccent - - readyall - - factions - - signallink - - adddamageflag - - removedamageflag - - godmode - - deleteewi - - hurt - - toggledisallowlatejoin - - showcontainedcontext - - hidecontainedcontext - - showmechanisms - - hidemechanisms - - attachbodypart - - attachtoself - - attachtogrid - - attachtograndparent - CanViewVar: true - CanAdminPlace: true - CanAdminMenu: true - -- Index: 200 - Name: Host - Commands: - - logout - - joingame - - help - - list - - say - - whisper - - me - - ooc - - showtime - - aghost - - observe - - toggleready - - ghost - - suicide - - spawn - - delete - - tp - - tpgrid - - setgamepreset - - forcepreset - - delaystart - - startround - - endround - - restartround - - respawn - - rejuvenate - - addcomp - - controlmob - - kick - - listplayers - - loc - - lsmap - - lsgrid - - mindinfo - - addrole - - rmrole - - addoverlay - - rmoverlay - - srvpopupmsg - - group - - showtime - - restart - - cvar - - netaudit - - szr_stats - - mem - - addai - - loglevel - - testlog - - addmap - - rmmap - - savebp - - loadbp - - savemap - - loadmap - - pausemap - - unpausemap - - querymappaused - - mapinit - - saveconfig - - gc - - gc_mode - - warp - - deleteewc - - sudo - - asay - - mapping - - addhand - - removehand - - tilepry - - anchor - - unanchor - - tubeconnections - - addatmos - - addgas - - fillgas - - listgases - - removegas - - settemp - - setatmostemp - - deletegas - - showatmos - - tilewalls - - events - - destroymechanism - - addaccent - - readyall - - factions - - signallink - - adddamageflag - - removedamageflag - - godmode - - deleteewi - - hurt - - toggledisallowlatejoin - - showcontainedcontext - - hidecontainedcontext - - showmechanisms - - hidemechanisms - - attachbodypart - - attachtoself - - attachtogrid - - attachtograndparent - CanViewVar: true - CanAdminPlace: true - CanScript: true - CanAdminMenu: true diff --git a/Resources/engineCommandPerms.yml b/Resources/engineCommandPerms.yml new file mode 100644 index 0000000000..80c10aafbf --- /dev/null +++ b/Resources/engineCommandPerms.yml @@ -0,0 +1,63 @@ +# Available to everybody +- Commands: + - help + - list + +- Flags: FUN + Commands: + - addcomp + - rmcomp + +- Flags: DEBUG + Commands: + - delete + - lsgrid + - lsmap + - listplayers + - loc + - mem + - netaudit + - querymappaused + - showtime + +- Flags: MAPPING + Commands: + - addmap + - loadbp + - loadmap + - pausemap + - querymappaused + - rmgrid + - rmmap + - mapinit + - savebp + - savemap + - tpgrid + +- Flags: ADMIN + Commands: + - delete + - kick + - listplayers + - teleport + +- Flags: SERVER + Commands: + - delete + - pausemap + - unpausemap + - restart + - shutdown + +- Flags: SPAWN + Commands: + - spawn + +- Flags: HOST + Commands: + - gc_mode + - gc + - loglevel + - saveconfig + - testlog + - sudo diff --git a/SpaceStation14.sln.DotSettings b/SpaceStation14.sln.DotSettings index 9a400dd8e1..c47b4913df 100644 --- a/SpaceStation14.sln.DotSettings +++ b/SpaceStation14.sln.DotSettings @@ -63,6 +63,7 @@ <data /> <data><IncludeFilters /><ExcludeFilters><Filter ModuleMask="*.UnitTesting" ModuleVersionMask="*" ClassMask="*" FunctionMask="*" IsEnabled="True" /></ExcludeFilters></data> True + True True True True From 637581bf3b2cfa3a5cc1fef0875c7abe40bed9a7 Mon Sep 17 00:00:00 2001 From: Pieter-Jan Briers Date: Sun, 1 Nov 2020 23:55:55 +0100 Subject: [PATCH 02/11] Correctly update the admin button the chat box. --- .../Administration/ClientAdminManager.cs | 4 ++ .../Administration/IClientAdminManager.cs | 5 +- Content.Client/Chat/ChatBox.cs | 17 ++--- Content.Client/Chat/ChatManager.cs | 63 ++++++++++++++----- 4 files changed, 60 insertions(+), 29 deletions(-) diff --git a/Content.Client/Administration/ClientAdminManager.cs b/Content.Client/Administration/ClientAdminManager.cs index eb5bfab65e..be0c18c974 100644 --- a/Content.Client/Administration/ClientAdminManager.cs +++ b/Content.Client/Administration/ClientAdminManager.cs @@ -19,6 +19,8 @@ namespace Content.Client.Administration private AdminData? _adminData; private readonly HashSet _availableCommands = new HashSet(); + public event Action? AdminStatusUpdated; + public bool HasFlag(AdminFlags flag) { return _adminData?.HasFlag(flag) ?? false; @@ -70,6 +72,8 @@ namespace Content.Client.Administration { Logger.InfoS("admin", $"Updated admin status: Not admin"); } + + AdminStatusUpdated?.Invoke(); } public event Action? ConGroupUpdated; diff --git a/Content.Client/Administration/IClientAdminManager.cs b/Content.Client/Administration/IClientAdminManager.cs index b6fca58ce2..533850dfaa 100644 --- a/Content.Client/Administration/IClientAdminManager.cs +++ b/Content.Client/Administration/IClientAdminManager.cs @@ -1,9 +1,12 @@ -using Content.Shared.Administration; +using System; +using Content.Shared.Administration; namespace Content.Client.Administration { public interface IClientAdminManager { + public event Action AdminStatusUpdated; + bool HasFlag(AdminFlags flag); bool CanCommand(string cmdName); diff --git a/Content.Client/Chat/ChatBox.cs b/Content.Client/Chat/ChatBox.cs index 7eb1befb17..627956a7d4 100644 --- a/Content.Client/Chat/ChatBox.cs +++ b/Content.Client/Chat/ChatBox.cs @@ -1,10 +1,8 @@ using Content.Shared.Chat; -using Robust.Client.Console; using Robust.Client.Graphics.Drawing; using Robust.Client.UserInterface; using Robust.Client.UserInterface.Controls; using Robust.Shared.Input; -using Robust.Shared.IoC; using Robust.Shared.Localization; using Robust.Shared.Maths; using Robust.Shared.Utility; @@ -97,16 +95,13 @@ namespace Content.Client.Chat ToggleMode = true, }; - var groupController = IoCManager.Resolve(); - if(groupController.CanCommand("asay")) + AdminButton = new Button { - AdminButton = new Button - { - Text = Loc.GetString("Admin"), - Name = "Admin", - ToggleMode = true, - }; - } + Text = Loc.GetString("Admin"), + Name = "Admin", + ToggleMode = true, + Visible = false + }; AllButton.OnToggled += OnFilterToggled; LocalButton.OnToggled += OnFilterToggled; diff --git a/Content.Client/Chat/ChatManager.cs b/Content.Client/Chat/ChatManager.cs index 7f935150cf..aa8014b5e5 100644 --- a/Content.Client/Chat/ChatManager.cs +++ b/Content.Client/Chat/ChatManager.cs @@ -1,6 +1,7 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; +using Content.Client.Administration; using Content.Client.Interfaces.Chat; +using Content.Shared.Administration; using Content.Shared.Chat; using Robust.Client.Console; using Robust.Client.Interfaces.Graphics.ClientEye; @@ -18,9 +19,11 @@ using Robust.Shared.Network; using Robust.Shared.Timing; using Robust.Shared.Utility; +#nullable enable + namespace Content.Client.Chat { - internal sealed class ChatManager : IChatManager + internal sealed class ChatManager : IChatManager, IPostInjectInit { private struct SpeechBubbleData { @@ -75,9 +78,10 @@ namespace Content.Client.Chat [Dependency] private readonly IEyeManager _eyeManager = default!; [Dependency] private readonly IUserInterfaceManager _userInterfaceManager = default!; [Dependency] private readonly IClientConGroupController _groupController = default!; + [Dependency] private readonly IClientAdminManager _adminMgr = default!; - private ChatBox _currentChatBox; - private Control _speechBubbleRoot; + private ChatBox? _currentChatBox; + private Control _speechBubbleRoot = null!; /// /// Speech bubbles that are currently visible on screen. @@ -103,7 +107,7 @@ namespace Content.Client.Chat _speechBubbleRoot.SetPositionFirst(); // When connexion is achieved, request the max chat message length - _netManager.Connected += new EventHandler(RequestMaxLength); + _netManager.Connected += RequestMaxLength; } public void FrameUpdate(FrameEventArgs delta) @@ -157,14 +161,15 @@ namespace Content.Client.Chat { _currentChatBox.TextSubmitted += _onChatBoxTextSubmitted; _currentChatBox.FilterToggled += _onFilterButtonToggled; + + _currentChatBox.AllButton.Pressed = !_allState; + _currentChatBox.LocalButton.Pressed = !_localState; + _currentChatBox.OOCButton.Pressed = !_oocState; + _currentChatBox.AdminButton.Pressed = !_adminState; + AdminStatusUpdated(); } RepopulateChat(filteredHistory); - _currentChatBox.AllButton.Pressed = !_allState; - _currentChatBox.LocalButton.Pressed = !_localState; - _currentChatBox.OOCButton.Pressed = !_oocState; - if(chatBox.AdminButton != null) - _currentChatBox.AdminButton.Pressed = !_adminState; } public void RemoveSpeechBubble(EntityUid entityUid, SpeechBubble bubble) @@ -229,9 +234,12 @@ namespace Content.Client.Chat // Check if message is longer than the character limit if (text.Length > _maxMessageLength) { - string locWarning = Loc.GetString("Your message exceeds {0} character limit", _maxMessageLength); - _currentChatBox?.AddLine(locWarning, ChatChannel.Server, Color.Orange); - _currentChatBox.ClearOnEnter = false; // The text shouldn't be cleared if it hasn't been sent + if (_currentChatBox != null) + { + string locWarning = Loc.GetString("Your message exceeds {0} character limit", _maxMessageLength); + _currentChatBox.AddLine(locWarning, ChatChannel.Server, Color.Orange); + _currentChatBox.ClearOnEnter = false; // The text shouldn't be cleared if it hasn't been sent + } return; } @@ -257,13 +265,15 @@ namespace Content.Client.Chat var conInput = text.Substring(1); if (string.IsNullOrWhiteSpace(conInput)) return; - if (_groupController.CanCommand("asay")){ + if (_groupController.CanCommand("asay")) + { _console.ProcessCommand($"asay \"{CommandParsing.Escape(conInput)}\""); } else { _console.ProcessCommand($"ooc \"{CommandParsing.Escape(conInput)}\""); } + break; } case MeAlias: @@ -276,7 +286,7 @@ namespace Content.Client.Chat } default: { - var conInput = _currentChatBox.DefaultChatFormat != null + var conInput = _currentChatBox?.DefaultChatFormat != null ? string.Format(_currentChatBox.DefaultChatFormat, CommandParsing.Escape(text)) : text; _console.ProcessCommand(conInput); @@ -341,6 +351,11 @@ namespace Content.Client.Chat private void RepopulateChat(IEnumerable filteredMessages) { + if (_currentChatBox == null) + { + return; + } + _currentChatBox.Contents.Clear(); foreach (var msg in filteredMessages) @@ -463,7 +478,8 @@ namespace Content.Client.Chat private void CreateSpeechBubble(IEntity entity, SpeechBubbleData speechData) { - var bubble = SpeechBubble.CreateSpeechBubble(speechData.Type, speechData.Message, entity, _eyeManager, this); + var bubble = + SpeechBubble.CreateSpeechBubble(speechData.Type, speechData.Message, entity, _eyeManager, this); if (_activeSpeechBubbles.TryGetValue(entity.Uid, out var existing)) { @@ -496,6 +512,19 @@ namespace Content.Client.Chat return _allState ^ _filteredChannels.HasFlag(channel); } + void IPostInjectInit.PostInject() + { + _adminMgr.AdminStatusUpdated += AdminStatusUpdated; + } + + private void AdminStatusUpdated() + { + if (_currentChatBox != null) + { + _currentChatBox.AdminButton.Visible = _adminMgr.HasFlag(AdminFlags.Admin); + } + } + private sealed class SpeechBubbleQueueData { /// From 6f7885164724d09b65dcc60a123884d4992fafe9 Mon Sep 17 00:00:00 2001 From: Pieter-Jan Briers Date: Sun, 1 Nov 2020 23:56:35 +0100 Subject: [PATCH 03/11] Send messages on admin/deadmin. --- Content.Server/Administration/AdminManager.cs | 11 +++++++--- Content.Server/Chat/ChatManager.cs | 22 ++++++++++++++++--- .../Interfaces/Chat/IChatManager.cs | 1 + 3 files changed, 28 insertions(+), 6 deletions(-) diff --git a/Content.Server/Administration/AdminManager.cs b/Content.Server/Administration/AdminManager.cs index 1b4862bfe6..119405c289 100644 --- a/Content.Server/Administration/AdminManager.cs +++ b/Content.Server/Administration/AdminManager.cs @@ -5,6 +5,7 @@ using System.Linq; using System.Net; using System.Reflection; using Content.Server.Database; +using Content.Server.Interfaces.Chat; using Content.Server.Players; using Content.Shared; using Content.Shared.Administration; @@ -35,6 +36,7 @@ namespace Content.Server.Administration [Dependency] private readonly IConGroupController _conGroup = default!; [Dependency] private readonly IResourceManager _res = default!; [Dependency] private readonly IConsoleShell _consoleShell = default!; + [Dependency] private readonly IChatManager _chat = default!; private readonly Dictionary _admins = new Dictionary(); @@ -69,12 +71,13 @@ namespace Content.Server.Administration return; } + _chat.SendAdminAnnouncement(Loc.GetString("{0} de-adminned themselves.", session.Name)); + _chat.DispatchServerMessage(session, Loc.GetString("You are now a normal player.")); + var plyData = session.ContentData()!; plyData.ExplicitlyDeadminned = true; reg.Data.Active = false; - // TODO: Send messages to all admins. - UpdateAdminStatus(session); } @@ -85,11 +88,13 @@ namespace Content.Server.Administration throw new ArgumentException($"Player {session} is not an admin"); } + _chat.DispatchServerMessage(session, Loc.GetString("You are now an admin.")); + var plyData = session.ContentData()!; plyData.ExplicitlyDeadminned = true; reg.Data.Active = true; - // TODO: Send messages to all admins. + _chat.SendAdminAnnouncement(Loc.GetString("{0} re-adminned themselves.", session.Name)); UpdateAdminStatus(session); } diff --git a/Content.Server/Chat/ChatManager.cs b/Content.Server/Chat/ChatManager.cs index df2b087372..1d35847a94 100644 --- a/Content.Server/Chat/ChatManager.cs +++ b/Content.Server/Chat/ChatManager.cs @@ -124,7 +124,7 @@ namespace Content.Server.Chat // Capitalize first letter message = message[0].ToString().ToUpper() + - message.Remove(0,1); + message.Remove(0, 1); if (source.TryGetComponent(out InventoryComponent inventory) && inventory.TryGetSlotItem(EquipmentSlotDefines.Slots.EARS, out ItemComponent item) && @@ -141,7 +141,7 @@ namespace Content.Server.Chat { // Capitalize first letter message = message[0].ToString().ToUpper() + - message.Remove(0,1); + message.Remove(0, 1); } var listeners = EntitySystem.Get(); @@ -211,7 +211,10 @@ namespace Content.Server.Chat return; } - var clients = _playerManager.GetPlayersBy(x => x.AttachedEntity != null && x.AttachedEntity.HasComponent()).Select(p => p.ConnectedClient);; + var clients = _playerManager + .GetPlayersBy(x => x.AttachedEntity != null && x.AttachedEntity.HasComponent()) + .Select(p => p.ConnectedClient); + ; var msg = _netManager.CreateNetMessage(); msg.Channel = ChatChannel.Dead; @@ -240,6 +243,19 @@ namespace Content.Server.Chat _netManager.ServerSendToMany(msg, clients.ToList()); } + public void SendAdminAnnouncement(string message) + { + var clients = _adminManager.ActiveAdmins.Select(p => p.ConnectedClient); + + var msg = _netManager.CreateNetMessage(); + + msg.Channel = ChatChannel.AdminChat; + msg.Message = message; + msg.MessageWrap = $"{Loc.GetString("ADMIN")}: {{0}}"; + + _netManager.ServerSendToMany(msg, clients.ToList()); + } + public void SendHookOOC(string sender, string message) { var msg = _netManager.CreateNetMessage(); diff --git a/Content.Server/Interfaces/Chat/IChatManager.cs b/Content.Server/Interfaces/Chat/IChatManager.cs index 9faee496eb..5dfcdbc458 100644 --- a/Content.Server/Interfaces/Chat/IChatManager.cs +++ b/Content.Server/Interfaces/Chat/IChatManager.cs @@ -31,5 +31,6 @@ namespace Content.Server.Interfaces.Chat delegate string TransformChat(IEntity speaker, string message); void RegisterChatTransform(TransformChat handler); + void SendAdminAnnouncement(string message); } } From 0618cde2b7025bb22cabf6252b331221452bf2e6 Mon Sep 17 00:00:00 2001 From: Pieter-Jan Briers Date: Mon, 2 Nov 2020 00:04:58 +0100 Subject: [PATCH 04/11] Announce logins/logouts. --- Content.Server/Administration/AdminManager.cs | 10 ++++++++++ Content.Shared/CCVars.cs | 10 ++++++++++ 2 files changed, 20 insertions(+) diff --git a/Content.Server/Administration/AdminManager.cs b/Content.Server/Administration/AdminManager.cs index 119405c289..28c5f93c30 100644 --- a/Content.Server/Administration/AdminManager.cs +++ b/Content.Server/Administration/AdminManager.cs @@ -203,6 +203,11 @@ namespace Content.Server.Administration else if (e.NewStatus == SessionStatus.Disconnected) { _admins.Remove(e.Session); + + if (_cfg.GetCVar(CCVars.AdminAnnounceLogout)) + { + _chat.SendAdminAnnouncement(Loc.GetString("Admin logout: {0}", e.Session.Name)); + } } } @@ -274,6 +279,11 @@ namespace Content.Server.Administration if (!session.ContentData()!.ExplicitlyDeadminned) { reg.Data.Active = true; + + if (_cfg.GetCVar(CCVars.AdminAnnounceLogin)) + { + _chat.SendAdminAnnouncement(Loc.GetString("Admin login: {0}", session.Name)); + } } UpdateAdminStatus(session); diff --git a/Content.Shared/CCVars.cs b/Content.Shared/CCVars.cs index 02d58f26bd..a7e2a79879 100644 --- a/Content.Shared/CCVars.cs +++ b/Content.Shared/CCVars.cs @@ -62,5 +62,15 @@ namespace Content.Shared public static readonly CVarDef DatabasePgPassword = CVarDef.Create("database.pg_password", "", CVar.SERVERONLY); + + /* + * Admin stuff + */ + + public static readonly CVarDef AdminAnnounceLogin = + CVarDef.Create("admin.announce_login", true, CVar.SERVERONLY); + + public static readonly CVarDef AdminAnnounceLogout = + CVarDef.Create("admin.announce_logout", true, CVar.SERVERONLY); } } From 6cc6cc369743c39ad12f294091180114ba30a4ba Mon Sep 17 00:00:00 2001 From: Pieter-Jan Briers Date: Mon, 2 Nov 2020 00:10:07 +0100 Subject: [PATCH 05/11] Make loginlocal SERVERONLY. --- Content.Shared/CCVars.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Content.Shared/CCVars.cs b/Content.Shared/CCVars.cs index a7e2a79879..bd1a620bed 100644 --- a/Content.Shared/CCVars.cs +++ b/Content.Shared/CCVars.cs @@ -35,7 +35,7 @@ namespace Content.Shared GamePersistGuests = CVarDef.Create("game.persistguests", true, CVar.ARCHIVE | CVar.SERVERONLY); public static readonly CVarDef - ConsoleLoginLocal = CVarDef.Create("console.loginlocal", true, CVar.ARCHIVE); + ConsoleLoginLocal = CVarDef.Create("console.loginlocal", true, CVar.ARCHIVE | CVar.SERVERONLY); /* From 0e419ad321a7f73433166249c783299b098acb2e Mon Sep 17 00:00:00 2001 From: Pieter-Jan Briers Date: Mon, 2 Nov 2020 00:22:53 +0100 Subject: [PATCH 06/11] Fix include calls for the admin DB stuff. --- Content.Server/Database/ServerDbBase.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Content.Server/Database/ServerDbBase.cs b/Content.Server/Database/ServerDbBase.cs index 7c0a13af63..60f4668acf 100644 --- a/Content.Server/Database/ServerDbBase.cs +++ b/Content.Server/Database/ServerDbBase.cs @@ -224,7 +224,11 @@ namespace Content.Server.Database { await using var db = await GetDb(); - return await db.DbContext.Admin.SingleOrDefaultAsync(p => p.UserId == userId.UserId); + return await db.DbContext.Admin + .Include(p => p.Flags) + .Include(p => p.AdminRank) + .ThenInclude(p => p.Flags) + .SingleOrDefaultAsync(p => p.UserId == userId.UserId); } protected abstract Task GetDb(); From 6980ff84b089b8c216d1178db0b3cece14f88448 Mon Sep 17 00:00:00 2001 From: Pieter-Jan Briers Date: Mon, 2 Nov 2020 00:23:34 +0100 Subject: [PATCH 07/11] Fix include calls for the admin DB stuff. --- Content.Server/Database/ServerDbBase.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Content.Server/Database/ServerDbBase.cs b/Content.Server/Database/ServerDbBase.cs index 60f4668acf..e09ebc014e 100644 --- a/Content.Server/Database/ServerDbBase.cs +++ b/Content.Server/Database/ServerDbBase.cs @@ -227,7 +227,7 @@ namespace Content.Server.Database return await db.DbContext.Admin .Include(p => p.Flags) .Include(p => p.AdminRank) - .ThenInclude(p => p.Flags) + .ThenInclude(p => p!.Flags) .SingleOrDefaultAsync(p => p.UserId == userId.UserId); } From e39ddd480226a1a52980ed44f33b7b4abcffa474 Mon Sep 17 00:00:00 2001 From: Pieter-Jan Briers Date: Tue, 10 Nov 2020 16:50:28 +0100 Subject: [PATCH 08/11] Permissions panel. --- .../Administration/ClientAdminManager.cs | 3 + .../Administration/IClientAdminManager.cs | 1 + Content.Client/ClientContentIoC.cs | 2 + Content.Client/EntryPoint.cs | 2 + Content.Client/Eui/BaseEui.cs | 53 ++ Content.Client/Eui/EuiManager.cs | 73 +++ .../Configuration/ConfigurationMenu.cs | 4 +- .../AdminMenu/AdminMenuManager.cs | 4 +- .../Permissions/PermissionsEui.cs | 601 ++++++++++++++++++ .../UserInterface/Stylesheets/StyleBase.cs | 10 + .../20201109092921_ExtraIndices.Designer.cs | 517 +++++++++++++++ .../Postgres/20201109092921_ExtraIndices.cs | 42 ++ .../PostgresServerDbContextModelSnapshot.cs | 8 + .../20201109092917_ExtraIndices.Designer.cs | 484 ++++++++++++++ .../Sqlite/20201109092917_ExtraIndices.cs | 42 ++ .../SqliteServerDbContextModelSnapshot.cs | 8 + Content.Server.Database/Model.cs | 8 + Content.Server.Database/ModelPostgres.cs | 3 + Content.Server.Database/ModelSqlite.cs | 8 + Content.Server/Administration/AdminManager.cs | 132 +++- .../AdminPermsChangedEventArgs.cs | 33 + .../Commands/OpenPermissionsCommand.cs | 31 + .../Administration/IAdminManager.cs | 31 +- .../Administration/PermissionsEui.cs | 460 ++++++++++++++ Content.Server/Database/PlayerRecord.cs | 29 + Content.Server/Database/ServerDbBase.cs | 75 ++- Content.Server/Database/ServerDbManager.cs | 76 ++- Content.Server/Database/ServerDbPostgres.cs | 62 ++ Content.Server/Database/ServerDbSqlite.cs | 54 ++ Content.Server/EntryPoint.cs | 9 + Content.Server/Eui/BaseEui.cs | 97 +++ Content.Server/Eui/EuiManager.cs | 144 +++++ Content.Server/ServerContentIoC.cs | 2 + Content.Shared/Administration/AdminFlags.cs | 2 +- .../Administration/AdminFlagsExt.cs | 16 + .../Administration/PermissionsEuiState.cs | 92 +++ Content.Shared/Eui/EuiMessageBase.cs | 10 + Content.Shared/Eui/EuiStateBase.cs | 11 + .../Network/NetMessages/MsgEuiCtl.cs | 55 ++ .../Network/NetMessages/MsgEuiMessage.cs | 48 ++ .../Network/NetMessages/MsgEuiState.cs | 48 ++ 41 files changed, 3355 insertions(+), 35 deletions(-) create mode 100644 Content.Client/Eui/BaseEui.cs create mode 100644 Content.Client/Eui/EuiManager.cs create mode 100644 Content.Client/UserInterface/Permissions/PermissionsEui.cs create mode 100644 Content.Server.Database/Migrations/Postgres/20201109092921_ExtraIndices.Designer.cs create mode 100644 Content.Server.Database/Migrations/Postgres/20201109092921_ExtraIndices.cs create mode 100644 Content.Server.Database/Migrations/Sqlite/20201109092917_ExtraIndices.Designer.cs create mode 100644 Content.Server.Database/Migrations/Sqlite/20201109092917_ExtraIndices.cs create mode 100644 Content.Server/Administration/AdminPermsChangedEventArgs.cs create mode 100644 Content.Server/Administration/Commands/OpenPermissionsCommand.cs create mode 100644 Content.Server/Administration/PermissionsEui.cs create mode 100644 Content.Server/Database/PlayerRecord.cs create mode 100644 Content.Server/Eui/BaseEui.cs create mode 100644 Content.Server/Eui/EuiManager.cs create mode 100644 Content.Shared/Administration/PermissionsEuiState.cs create mode 100644 Content.Shared/Eui/EuiMessageBase.cs create mode 100644 Content.Shared/Eui/EuiStateBase.cs create mode 100644 Content.Shared/Network/NetMessages/MsgEuiCtl.cs create mode 100644 Content.Shared/Network/NetMessages/MsgEuiMessage.cs create mode 100644 Content.Shared/Network/NetMessages/MsgEuiState.cs diff --git a/Content.Client/Administration/ClientAdminManager.cs b/Content.Client/Administration/ClientAdminManager.cs index be0c18c974..96b644579c 100644 --- a/Content.Client/Administration/ClientAdminManager.cs +++ b/Content.Client/Administration/ClientAdminManager.cs @@ -21,6 +21,8 @@ namespace Content.Client.Administration public event Action? AdminStatusUpdated; + public AdminFlags? Flags => _adminData?.Flags; + public bool HasFlag(AdminFlags flag) { return _adminData?.HasFlag(flag) ?? false; @@ -74,6 +76,7 @@ namespace Content.Client.Administration } AdminStatusUpdated?.Invoke(); + ConGroupUpdated?.Invoke(); } public event Action? ConGroupUpdated; diff --git a/Content.Client/Administration/IClientAdminManager.cs b/Content.Client/Administration/IClientAdminManager.cs index 533850dfaa..1bdf0cb029 100644 --- a/Content.Client/Administration/IClientAdminManager.cs +++ b/Content.Client/Administration/IClientAdminManager.cs @@ -7,6 +7,7 @@ namespace Content.Client.Administration { public event Action AdminStatusUpdated; + AdminFlags? Flags { get; } bool HasFlag(AdminFlags flag); bool CanCommand(string cmdName); diff --git a/Content.Client/ClientContentIoC.cs b/Content.Client/ClientContentIoC.cs index 9e353cee87..841ed7d44a 100644 --- a/Content.Client/ClientContentIoC.cs +++ b/Content.Client/ClientContentIoC.cs @@ -1,5 +1,6 @@ using Content.Client.Administration; using Content.Client.Chat; +using Content.Client.Eui; using Content.Client.GameTicking; using Content.Client.Interfaces; using Content.Client.Interfaces.Chat; @@ -37,6 +38,7 @@ namespace Content.Client IoCManager.Register(); IoCManager.Register(); IoCManager.Register(); + IoCManager.Register(); } } } diff --git a/Content.Client/EntryPoint.cs b/Content.Client/EntryPoint.cs index 8512a2231a..d3fa1057f8 100644 --- a/Content.Client/EntryPoint.cs +++ b/Content.Client/EntryPoint.cs @@ -1,5 +1,6 @@ using System; using Content.Client.Administration; +using Content.Client.Eui; using Content.Client.GameObjects.Components.Actor; using Content.Client.Input; using Content.Client.Interfaces; @@ -154,6 +155,7 @@ namespace Content.Client IoCManager.Resolve().Initialize(); IoCManager.Resolve().Initialize(); IoCManager.Resolve().Initialize(); + IoCManager.Resolve().Initialize(); _baseClient.RunLevelChanged += (sender, args) => { diff --git a/Content.Client/Eui/BaseEui.cs b/Content.Client/Eui/BaseEui.cs new file mode 100644 index 0000000000..65ebe3d213 --- /dev/null +++ b/Content.Client/Eui/BaseEui.cs @@ -0,0 +1,53 @@ +using Content.Shared.Eui; +using Content.Shared.Network.NetMessages; +using Robust.Shared.Interfaces.Network; +using Robust.Shared.IoC; + +#nullable enable + +namespace Content.Client.Eui +{ + public abstract class BaseEui + { + [Dependency] private readonly IClientNetManager _netManager = default!; + + public EuiManager Manager { get; private set; } = default!; + public uint Id { get; private set; } + + protected BaseEui() + { + IoCManager.InjectDependencies(this); + } + + public void Initialize(EuiManager mgr, uint id) + { + Manager = mgr; + Id = id; + } + + public virtual void Opened() + { + } + + public virtual void Closed() + { + } + + public virtual void HandleState(EuiStateBase state) + { + } + + public virtual void HandleMessage(EuiMessageBase msg) + { + } + + protected void SendMessage(EuiMessageBase msg) + { + var netMsg = _netManager.CreateNetMessage(); + netMsg.Id = Id; + netMsg.Message = msg; + + _netManager.ClientSendMessage(netMsg); + } + } +} diff --git a/Content.Client/Eui/EuiManager.cs b/Content.Client/Eui/EuiManager.cs new file mode 100644 index 0000000000..60e4d84047 --- /dev/null +++ b/Content.Client/Eui/EuiManager.cs @@ -0,0 +1,73 @@ +using System; +using System.Collections.Generic; +using Content.Shared.Network.NetMessages; +using Robust.Shared.Interfaces.Network; +using Robust.Shared.Interfaces.Reflection; +using Robust.Shared.IoC; + +#nullable enable + +namespace Content.Client.Eui +{ + public sealed class EuiManager + { + [Dependency] private readonly IClientNetManager _net = default!; + [Dependency] private readonly IReflectionManager _refl = default!; + [Dependency] private readonly IDynamicTypeFactory _dtf = default!; + + private readonly Dictionary _openUis = new Dictionary(); + + public void Initialize() + { + _net.RegisterNetMessage(MsgEuiCtl.NAME, RxMsgCtl); + _net.RegisterNetMessage(MsgEuiState.NAME, RxMsgState); + _net.RegisterNetMessage(MsgEuiMessage.NAME, RxMsgMessage); + } + + private void RxMsgMessage(MsgEuiMessage message) + { + var ui = _openUis[message.Id].Eui; + ui.HandleMessage(message.Message); + } + + private void RxMsgState(MsgEuiState message) + { + var ui = _openUis[message.Id].Eui; + ui.HandleState(message.State); + } + + private void RxMsgCtl(MsgEuiCtl message) + { + switch (message.Type) + { + case MsgEuiCtl.CtlType.Open: + var euiType = _refl.LooseGetType(message.OpenType); + var instance = _dtf.CreateInstance(euiType); + instance.Initialize(this, message.Id); + instance.Opened(); + _openUis.Add(message.Id, new EuiData(instance)); + break; + + case MsgEuiCtl.CtlType.Close: + var dat = _openUis[message.Id]; + dat.Eui.Closed(); + _openUis.Remove(message.Id); + + break; + + default: + throw new ArgumentOutOfRangeException(); + } + } + + private sealed class EuiData + { + public BaseEui Eui; + + public EuiData(BaseEui eui) + { + Eui = eui; + } + } + } +} diff --git a/Content.Client/GameObjects/Components/Configuration/ConfigurationMenu.cs b/Content.Client/GameObjects/Components/Configuration/ConfigurationMenu.cs index d236ae0202..f3a2141d60 100644 --- a/Content.Client/GameObjects/Components/Configuration/ConfigurationMenu.cs +++ b/Content.Client/GameObjects/Components/Configuration/ConfigurationMenu.cs @@ -101,7 +101,7 @@ namespace Content.Client.GameObjects.Components.Wires { _column.Children.Clear(); _inputs.Clear(); - + foreach (var field in state.Config) { var margin = new MarginContainer @@ -143,7 +143,7 @@ namespace Content.Client.GameObjects.Components.Wires private void OnConfirm(ButtonEventArgs args) { var config = GenerateDictionary(_inputs, "Text"); - + Owner.SendConfiguration(config); Close(); } diff --git a/Content.Client/UserInterface/AdminMenu/AdminMenuManager.cs b/Content.Client/UserInterface/AdminMenu/AdminMenuManager.cs index ebcb5cc7fb..94b29253cf 100644 --- a/Content.Client/UserInterface/AdminMenu/AdminMenuManager.cs +++ b/Content.Client/UserInterface/AdminMenu/AdminMenuManager.cs @@ -11,14 +11,14 @@ namespace Content.Client.UserInterface.AdminMenu { internal class AdminMenuManager : IAdminMenuManager { - [Dependency] private INetManager _netManager = default!; + [Dependency] private readonly INetManager _netManager = default!; [Dependency] private readonly IInputManager _inputManager = default!; [Dependency] private readonly IClientConGroupController _clientConGroupController = default!; private SS14Window _window; private List _commandWindows; - public void Initialize() + public void Initialize() { _commandWindows = new List(); // Reset the AdminMenu Window on disconnect diff --git a/Content.Client/UserInterface/Permissions/PermissionsEui.cs b/Content.Client/UserInterface/Permissions/PermissionsEui.cs new file mode 100644 index 0000000000..4801472f3e --- /dev/null +++ b/Content.Client/UserInterface/Permissions/PermissionsEui.cs @@ -0,0 +1,601 @@ +using System.Collections.Generic; +using System.Linq; +using Content.Client.Administration; +using Content.Client.Eui; +using Content.Client.UserInterface.Stylesheets; +using Content.Shared.Administration; +using Content.Shared.Eui; +using JetBrains.Annotations; +using Robust.Client.UserInterface; +using Robust.Client.UserInterface.Controls; +using Robust.Client.UserInterface.CustomControls; +using Robust.Shared.IoC; +using Robust.Shared.Localization; +using Robust.Shared.Maths; +using Robust.Shared.Utility; +using static Content.Shared.Administration.PermissionsEuiMsg; + +#nullable enable + +namespace Content.Client.UserInterface.Permissions +{ + [UsedImplicitly] + public sealed class PermissionsEui : BaseEui + { + private const int NoRank = -1; + + [Dependency] private readonly IClientAdminManager _adminManager = default!; + + private readonly Menu _menu; + private readonly List _subWindows = new List(); + + private Dictionary _ranks = + new Dictionary(); + + public PermissionsEui() + { + IoCManager.InjectDependencies(this); + + _menu = new Menu(this); + _menu.AddAdminButton.OnPressed += AddAdminPressed; + _menu.AddAdminRankButton.OnPressed += AddAdminRankPressed; + _menu.OnClose += CloseEverything; + } + + public override void Closed() + { + base.Closed(); + + CloseEverything(); + } + + private void CloseEverything() + { + foreach (var subWindow in _subWindows.ToArray()) + { + subWindow.Close(); + } + + _menu.Close(); + } + + private void AddAdminPressed(BaseButton.ButtonEventArgs obj) + { + OpenEditWindow(null); + } + + private void AddAdminRankPressed(BaseButton.ButtonEventArgs obj) + { + OpenRankEditWindow(null); + } + + + private void OnEditPressed(PermissionsEuiState.AdminData admin) + { + OpenEditWindow(admin); + } + + private void OpenEditWindow(PermissionsEuiState.AdminData? data) + { + var window = new EditAdminWindow(this, data); + window.SaveButton.OnPressed += _ => SaveAdminPressed(window); + window.OpenCentered(); + window.OnClose += () => _subWindows.Remove(window); + if (data != null) + { + window.RemoveButton!.OnPressed += _ => RemoveButtonPressed(window); + } + + _subWindows.Add(window); + } + + + private void OpenRankEditWindow(KeyValuePair? rank) + { + var window = new EditAdminRankWindow(this, rank); + window.SaveButton.OnPressed += _ => SaveAdminRankPressed(window); + window.OpenCentered(); + window.OnClose += () => _subWindows.Remove(window); + if (rank != null) + { + window.RemoveButton!.OnPressed += _ => RemoveRankButtonPressed(window); + } + + _subWindows.Add(window); + } + + private void RemoveButtonPressed(EditAdminWindow window) + { + SendMessage(new RemoveAdmin {UserId = window.SourceData!.Value.UserId}); + + window.Close(); + } + + private void RemoveRankButtonPressed(EditAdminRankWindow window) + { + SendMessage(new RemoveAdminRank {Id = window.SourceId!.Value}); + + window.Close(); + } + + private void SaveAdminPressed(EditAdminWindow popup) + { + popup.CollectSetFlags(out var pos, out var neg); + + int? rank = popup.RankButton.SelectedId; + if (rank == NoRank) + { + rank = null; + } + + var title = string.IsNullOrWhiteSpace(popup.TitleEdit.Text) ? null : popup.TitleEdit.Text; + + if (popup.SourceData is { } src) + { + SendMessage(new UpdateAdmin + { + UserId = src.UserId, + Title = title, + PosFlags = pos, + NegFlags = neg, + RankId = rank + }); + } + else + { + DebugTools.AssertNotNull(popup.NameEdit); + + SendMessage(new AddAdmin + { + UserNameOrId = popup.NameEdit!.Text, + Title = title, + PosFlags = pos, + NegFlags = neg, + RankId = rank + }); + } + + popup.Close(); + } + + + private void SaveAdminRankPressed(EditAdminRankWindow popup) + { + var flags = popup.CollectSetFlags(); + var name = popup.NameEdit.Text; + + if (popup.SourceId is { } src) + { + SendMessage(new UpdateAdminRank + { + Id = src, + Flags = flags, + Name = name + }); + } + else + { + SendMessage(new AddAdminRank + { + Flags = flags, + Name = name + }); + } + + popup.Close(); + } + + public override void Opened() + { + _menu.OpenCentered(); + } + + public override void HandleState(EuiStateBase state) + { + var s = (PermissionsEuiState) state; + + if (s.IsLoading) + { + return; + } + + _ranks = s.AdminRanks; + + _menu.AdminsList.RemoveAllChildren(); + foreach (var admin in s.Admins) + { + var al = _menu.AdminsList; + var name = admin.UserName ?? admin.UserId.ToString(); + + al.AddChild(new Label {Text = name}); + + var titleControl = new Label {Text = admin.Title ?? Loc.GetString("none")}; + if (admin.Title == null) // none + { + titleControl.StyleClasses.Add(StyleBase.StyleClassItalic); + } + + al.AddChild(titleControl); + + bool italic; + string rank; + var combinedFlags = admin.PosFlags; + if (admin.RankId is { } rankId) + { + italic = false; + var rankData = s.AdminRanks[rankId]; + rank = rankData.Name; + combinedFlags |= rankData.Flags; + } + else + { + italic = true; + rank = Loc.GetString("none"); + } + + var rankControl = new Label {Text = rank}; + if (italic) + { + rankControl.StyleClasses.Add(StyleBase.StyleClassItalic); + } + + al.AddChild(rankControl); + + var flagsText = AdminFlagsExt.PosNegFlagsText(admin.PosFlags, admin.NegFlags); + + al.AddChild(new Label + { + Text = flagsText, + SizeFlagsHorizontal = Control.SizeFlags.ShrinkCenter | Control.SizeFlags.Expand + }); + + var editButton = new Button {Text = Loc.GetString("Edit")}; + editButton.OnPressed += _ => OnEditPressed(admin); + al.AddChild(editButton); + + if (!_adminManager.HasFlag(combinedFlags)) + { + editButton.Disabled = true; + editButton.ToolTip = Loc.GetString("You do not have the required flags to edit this admin."); + } + } + + _menu.AdminRanksList.RemoveAllChildren(); + foreach (var kv in s.AdminRanks) + { + var rank = kv.Value; + var flagsText = string.Join(' ', AdminFlagsExt.FlagsToNames(rank.Flags).Select(f => $"+{f}")); + _menu.AdminRanksList.AddChild(new Label {Text = rank.Name}); + _menu.AdminRanksList.AddChild(new Label + { + Text = flagsText, + SizeFlagsHorizontal = Control.SizeFlags.ShrinkCenter | Control.SizeFlags.Expand + }); + var editButton = new Button {Text = Loc.GetString("Edit")}; + editButton.OnPressed += _ => OnEditRankPressed(kv); + _menu.AdminRanksList.AddChild(editButton); + + if (!_adminManager.HasFlag(rank.Flags)) + { + editButton.Disabled = true; + editButton.ToolTip = Loc.GetString("You do not have the required flags to edit this rank."); + } + } + } + + private void OnEditRankPressed(KeyValuePair rank) + { + OpenRankEditWindow(rank); + } + + private sealed class Menu : SS14Window + { + private readonly PermissionsEui _ui; + public readonly GridContainer AdminsList; + public readonly GridContainer AdminRanksList; + public readonly Button AddAdminButton; + public readonly Button AddAdminRankButton; + + public Menu(PermissionsEui ui) + { + _ui = ui; + Title = Loc.GetString("Permissions Panel"); + + var tab = new TabContainer(); + + AddAdminButton = new Button + { + Text = Loc.GetString("Add Admin"), + SizeFlagsHorizontal = SizeFlags.ShrinkEnd + }; + + AddAdminRankButton = new Button + { + Text = Loc.GetString("Add Admin Rank"), + SizeFlagsHorizontal = SizeFlags.ShrinkEnd + }; + + AdminsList = new GridContainer {Columns = 5, SizeFlagsVertical = SizeFlags.FillExpand}; + var adminVBox = new VBoxContainer + { + Children = {AdminsList, AddAdminButton}, + }; + TabContainer.SetTabTitle(adminVBox, Loc.GetString("Admins")); + + AdminRanksList = new GridContainer {Columns = 3}; + var rankVBox = new VBoxContainer + { + Children = { AdminRanksList, AddAdminRankButton} + }; + TabContainer.SetTabTitle(rankVBox, Loc.GetString("Admin Ranks")); + + tab.AddChild(adminVBox); + tab.AddChild(rankVBox); + + Contents.AddChild(tab); + } + + protected override Vector2 ContentsMinimumSize => (600, 400); + } + + private sealed class EditAdminWindow : SS14Window + { + public readonly PermissionsEuiState.AdminData? SourceData; + public readonly LineEdit? NameEdit; + public readonly LineEdit TitleEdit; + public readonly OptionButton RankButton; + public readonly Button SaveButton; + public readonly Button? RemoveButton; + + public readonly Dictionary FlagButtons + = new Dictionary(); + + public EditAdminWindow(PermissionsEui ui, PermissionsEuiState.AdminData? data) + { + SourceData = data; + + Control nameControl; + + if (data is { } dat) + { + var name = dat.UserName ?? dat.UserId.ToString(); + Title = Loc.GetString("Edit admin {0}", name); + + nameControl = new Label {Text = name}; + } + else + { + Title = Loc.GetString("Add admin"); + + nameControl = NameEdit = new LineEdit {PlaceHolder = Loc.GetString("Username/User ID")}; + } + + TitleEdit = new LineEdit {PlaceHolder = Loc.GetString("Custom title, leave blank to inherit rank title.")}; + RankButton = new OptionButton(); + SaveButton = new Button {Text = Loc.GetString("Save"), SizeFlagsHorizontal = SizeFlags.ShrinkEnd | SizeFlags.Expand}; + + RankButton.AddItem(Loc.GetString("No rank"), NoRank); + foreach (var (rId, rank) in ui._ranks) + { + RankButton.AddItem(rank.Name, rId); + } + + RankButton.SelectId(data?.RankId ?? NoRank); + RankButton.OnItemSelected += RankSelected; + + var permGrid = new GridContainer + { + Columns = 4, + HSeparationOverride = 0, + VSeparationOverride = 0 + }; + + foreach (var flag in AdminFlagsExt.AllFlags) + { + // Can only grant out perms you also have yourself. + // Primarily intended to prevent people giving themselves +HOST with +PERMISSIONS but generalized. + var disable = !ui._adminManager.HasFlag(flag); + var flagName = flag.ToString().ToUpper(); + + var group = new ButtonGroup(); + + var inherit = new Button + { + Text = "I", + StyleClasses = {StyleBase.ButtonOpenRight}, + Disabled = disable, + Group = group, + }; + var sub = new Button + { + Text = "-", + StyleClasses = {StyleBase.ButtonOpenBoth}, + Disabled = disable, + Group = group + }; + var plus = new Button + { + Text = "+", + StyleClasses = {StyleBase.ButtonOpenLeft}, + Disabled = disable, + Group = group + }; + + if (data is { } d) + { + if ((d.NegFlags & flag) != 0) + { + sub.Pressed = true; + } + else if ((d.PosFlags & flag) != 0) + { + plus.Pressed = true; + } + else + { + inherit.Pressed = true; + } + } + else + { + inherit.Pressed = true; + } + + permGrid.AddChild(new Label {Text = flagName}); + permGrid.AddChild(inherit); + permGrid.AddChild(sub); + permGrid.AddChild(plus); + + FlagButtons.Add(flag, (inherit, sub, plus)); + } + + var bottomButtons = new HBoxContainer(); + if (data != null) + { + // show remove button. + RemoveButton = new Button {Text = Loc.GetString("Remove")}; + bottomButtons.AddChild(RemoveButton); + } + + bottomButtons.AddChild(SaveButton); + + Contents.AddChild(new VBoxContainer + { + Children = + { + new HBoxContainer + { + SeparationOverride = 2, + Children = + { + new VBoxContainer + { + SizeFlagsHorizontal = SizeFlags.FillExpand, + Children = + { + nameControl, + TitleEdit, + RankButton + } + }, + permGrid + }, + SizeFlagsVertical = SizeFlags.FillExpand + }, + bottomButtons + } + }); + } + + private void RankSelected(OptionButton.ItemSelectedEventArgs obj) + { + RankButton.SelectId(obj.Id); + } + + public void CollectSetFlags(out AdminFlags pos, out AdminFlags neg) + { + pos = default; + neg = default; + + foreach (var (flag, (_, s, p)) in FlagButtons) + { + if (s.Pressed) + { + neg |= flag; + } + else if (p.Pressed) + { + pos |= flag; + } + } + } + + protected override Vector2? CustomSize => (600, 400); + } + + private sealed class EditAdminRankWindow : SS14Window + { + public readonly int? SourceId; + public readonly LineEdit NameEdit; + public readonly Button SaveButton; + public readonly Button? RemoveButton; + public readonly Dictionary FlagCheckBoxes = new Dictionary(); + + public EditAdminRankWindow(PermissionsEui ui, KeyValuePair? data) + { + SourceId = data?.Key; + + NameEdit = new LineEdit + { + PlaceHolder = "Rank name", + }; + + if (data != null) + { + NameEdit.Text = data.Value.Value.Name; + } + + SaveButton = new Button {Text = Loc.GetString("Save"), SizeFlagsHorizontal = SizeFlags.ShrinkEnd | SizeFlags.Expand}; + var flagsBox = new VBoxContainer(); + + foreach (var flag in AdminFlagsExt.AllFlags) + { + // Can only grant out perms you also have yourself. + // Primarily intended to prevent people giving themselves +HOST with +PERMISSIONS but generalized. + var disable = !ui._adminManager.HasFlag(flag); + var flagName = flag.ToString().ToUpper(); + + var checkBox = new CheckBox + { + Disabled = disable, + Text = flagName + }; + + if (data != null && (data.Value.Value.Flags & flag) != 0) + { + checkBox.Pressed = true; + } + + FlagCheckBoxes.Add(flag, checkBox); + flagsBox.AddChild(checkBox); + } + + var bottomButtons = new HBoxContainer(); + if (data != null) + { + // show remove button. + RemoveButton = new Button {Text = Loc.GetString("Remove")}; + bottomButtons.AddChild(RemoveButton); + } + + bottomButtons.AddChild(SaveButton); + + Contents.AddChild(new VBoxContainer + { + Children = + { + NameEdit, + flagsBox, + bottomButtons + } + }); + } + + public AdminFlags CollectSetFlags() + { + AdminFlags flags = default; + foreach (var (flag, chk) in FlagCheckBoxes) + { + if (chk.Pressed) + { + flags |= flag; + } + } + + return flags; + } + + protected override Vector2? CustomSize => (600, 400); + } + } +} diff --git a/Content.Client/UserInterface/Stylesheets/StyleBase.cs b/Content.Client/UserInterface/Stylesheets/StyleBase.cs index 1cc9aa0769..83dd9c26cb 100644 --- a/Content.Client/UserInterface/Stylesheets/StyleBase.cs +++ b/Content.Client/UserInterface/Stylesheets/StyleBase.cs @@ -12,6 +12,7 @@ namespace Content.Client.UserInterface.Stylesheets public const string ClassHighDivider = "HighDivider"; public const string StyleClassLabelHeading = "LabelHeading"; public const string StyleClassLabelSubText = "LabelSubText"; + public const string StyleClassItalic = "Italic"; public const string ButtonOpenRight = "OpenRight"; public const string ButtonOpenLeft = "OpenLeft"; @@ -31,6 +32,7 @@ namespace Content.Client.UserInterface.Stylesheets protected StyleBase(IResourceCache resCache) { var notoSans12 = resCache.GetFont("/Fonts/NotoSans/NotoSans-Regular.ttf", 12); + var notoSans12Italic = resCache.GetFont("/Fonts/NotoSans/NotoSans-Italic.ttf", 12); // Button styles. var buttonTex = resCache.GetTexture("/Textures/Interface/Nano/button.svg.96dpi.png"); @@ -77,6 +79,14 @@ namespace Content.Client.UserInterface.Stylesheets { new StyleProperty("font", notoSans12), }), + + // Default font. + new StyleRule( + new SelectorElement(null, new[] {StyleClassItalic}, null, null), + new[] + { + new StyleProperty("font", notoSans12Italic), + }), }; } } diff --git a/Content.Server.Database/Migrations/Postgres/20201109092921_ExtraIndices.Designer.cs b/Content.Server.Database/Migrations/Postgres/20201109092921_ExtraIndices.Designer.cs new file mode 100644 index 0000000000..ce41855793 --- /dev/null +++ b/Content.Server.Database/Migrations/Postgres/20201109092921_ExtraIndices.Designer.cs @@ -0,0 +1,517 @@ +// +using System; +using System.Net; +using Content.Server.Database; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +namespace Content.Server.Database.Migrations.Postgres +{ + [DbContext(typeof(PostgresServerDbContext))] + [Migration("20201109092921_ExtraIndices")] + partial class ExtraIndices + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn) + .HasAnnotation("ProductVersion", "3.1.4") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + modelBuilder.Entity("Content.Server.Database.Admin", b => + { + b.Property("UserId") + .ValueGeneratedOnAdd() + .HasColumnName("user_id") + .HasColumnType("uuid"); + + b.Property("AdminRankId") + .HasColumnName("admin_rank_id") + .HasColumnType("integer"); + + b.Property("Title") + .HasColumnName("title") + .HasColumnType("text"); + + b.HasKey("UserId"); + + b.HasIndex("AdminRankId"); + + b.ToTable("admin"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminFlag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("admin_flag_id") + .HasColumnType("integer") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("AdminId") + .HasColumnName("admin_id") + .HasColumnType("uuid"); + + b.Property("Flag") + .IsRequired() + .HasColumnName("flag") + .HasColumnType("text"); + + b.Property("Negative") + .HasColumnName("negative") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.HasIndex("AdminId"); + + b.HasIndex("Flag", "AdminId") + .IsUnique(); + + b.ToTable("admin_flag"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminRank", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("admin_rank_id") + .HasColumnType("integer") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("Name") + .IsRequired() + .HasColumnName("name") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("admin_rank"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminRankFlag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("admin_rank_flag_id") + .HasColumnType("integer") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("AdminRankId") + .HasColumnName("admin_rank_id") + .HasColumnType("integer"); + + b.Property("Flag") + .IsRequired() + .HasColumnName("flag") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("AdminRankId"); + + b.HasIndex("Flag", "AdminRankId") + .IsUnique(); + + b.ToTable("admin_rank_flag"); + }); + + modelBuilder.Entity("Content.Server.Database.Antag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("antag_id") + .HasColumnType("integer") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("AntagName") + .IsRequired() + .HasColumnName("antag_name") + .HasColumnType("text"); + + b.Property("ProfileId") + .HasColumnName("profile_id") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("ProfileId", "AntagName") + .IsUnique(); + + b.ToTable("antag"); + }); + + modelBuilder.Entity("Content.Server.Database.AssignedUserId", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("assigned_user_id_id") + .HasColumnType("integer") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("UserId") + .HasColumnName("user_id") + .HasColumnType("uuid"); + + b.Property("UserName") + .IsRequired() + .HasColumnName("user_name") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("UserId") + .IsUnique(); + + b.HasIndex("UserName") + .IsUnique(); + + b.ToTable("assigned_user_id"); + }); + + modelBuilder.Entity("Content.Server.Database.Job", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("job_id") + .HasColumnType("integer") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("JobName") + .IsRequired() + .HasColumnName("job_name") + .HasColumnType("text"); + + b.Property("Priority") + .HasColumnName("priority") + .HasColumnType("integer"); + + b.Property("ProfileId") + .HasColumnName("profile_id") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("ProfileId"); + + b.ToTable("job"); + }); + + modelBuilder.Entity("Content.Server.Database.PostgresConnectionLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("connection_log_id") + .HasColumnType("integer") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("Address") + .IsRequired() + .HasColumnName("address") + .HasColumnType("inet"); + + b.Property("Time") + .HasColumnName("time") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnName("user_id") + .HasColumnType("uuid"); + + b.Property("UserName") + .IsRequired() + .HasColumnName("user_name") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("connection_log"); + + b.HasCheckConstraint("AddressNotIPv6MappedIPv4", "NOT inet '::ffff:0.0.0.0/96' >>= address"); + }); + + modelBuilder.Entity("Content.Server.Database.PostgresPlayer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("player_id") + .HasColumnType("integer") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("FirstSeenTime") + .HasColumnName("first_seen_time") + .HasColumnType("timestamp with time zone"); + + b.Property("LastSeenAddress") + .IsRequired() + .HasColumnName("last_seen_address") + .HasColumnType("inet"); + + b.Property("LastSeenTime") + .HasColumnName("last_seen_time") + .HasColumnType("timestamp with time zone"); + + b.Property("LastSeenUserName") + .IsRequired() + .HasColumnName("last_seen_user_name") + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnName("user_id") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("LastSeenUserName"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("player"); + + b.HasCheckConstraint("LastSeenAddressNotIPv6MappedIPv4", "NOT inet '::ffff:0.0.0.0/96' >>= last_seen_address"); + }); + + modelBuilder.Entity("Content.Server.Database.PostgresServerBan", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("server_ban_id") + .HasColumnType("integer") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property?>("Address") + .HasColumnName("address") + .HasColumnType("inet"); + + b.Property("BanTime") + .HasColumnName("ban_time") + .HasColumnType("timestamp with time zone"); + + b.Property("BanningAdmin") + .HasColumnName("banning_admin") + .HasColumnType("uuid"); + + b.Property("ExpirationTime") + .HasColumnName("expiration_time") + .HasColumnType("timestamp with time zone"); + + b.Property("Reason") + .IsRequired() + .HasColumnName("reason") + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnName("user_id") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("Address"); + + b.HasIndex("UserId"); + + b.ToTable("server_ban"); + + b.HasCheckConstraint("AddressNotIPv6MappedIPv4", "NOT inet '::ffff:0.0.0.0/96' >>= address"); + + b.HasCheckConstraint("HaveEitherAddressOrUserId", "address IS NOT NULL OR user_id IS NOT NULL"); + }); + + modelBuilder.Entity("Content.Server.Database.PostgresServerUnban", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("unban_id") + .HasColumnType("integer") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("BanId") + .HasColumnName("ban_id") + .HasColumnType("integer"); + + b.Property("UnbanTime") + .HasColumnName("unban_time") + .HasColumnType("timestamp with time zone"); + + b.Property("UnbanningAdmin") + .HasColumnName("unbanning_admin") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("BanId") + .IsUnique(); + + b.ToTable("server_unban"); + }); + + modelBuilder.Entity("Content.Server.Database.Preference", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("preference_id") + .HasColumnType("integer") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("SelectedCharacterSlot") + .HasColumnName("selected_character_slot") + .HasColumnType("integer"); + + b.Property("UserId") + .HasColumnName("user_id") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("preference"); + }); + + modelBuilder.Entity("Content.Server.Database.Profile", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("profile_id") + .HasColumnType("integer") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("Age") + .HasColumnName("age") + .HasColumnType("integer"); + + b.Property("CharacterName") + .IsRequired() + .HasColumnName("char_name") + .HasColumnType("text"); + + b.Property("EyeColor") + .IsRequired() + .HasColumnName("eye_color") + .HasColumnType("text"); + + b.Property("FacialHairColor") + .IsRequired() + .HasColumnName("facial_hair_color") + .HasColumnType("text"); + + b.Property("FacialHairName") + .IsRequired() + .HasColumnName("facial_hair_name") + .HasColumnType("text"); + + b.Property("HairColor") + .IsRequired() + .HasColumnName("hair_color") + .HasColumnType("text"); + + b.Property("HairName") + .IsRequired() + .HasColumnName("hair_name") + .HasColumnType("text"); + + b.Property("PreferenceId") + .HasColumnName("preference_id") + .HasColumnType("integer"); + + b.Property("PreferenceUnavailable") + .HasColumnName("pref_unavailable") + .HasColumnType("integer"); + + b.Property("Sex") + .IsRequired() + .HasColumnName("sex") + .HasColumnType("text"); + + b.Property("SkinColor") + .IsRequired() + .HasColumnName("skin_color") + .HasColumnType("text"); + + b.Property("Slot") + .HasColumnName("slot") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("PreferenceId"); + + b.HasIndex("Slot", "PreferenceId") + .IsUnique(); + + b.ToTable("profile"); + }); + + modelBuilder.Entity("Content.Server.Database.Admin", b => + { + b.HasOne("Content.Server.Database.AdminRank", "AdminRank") + .WithMany("Admins") + .HasForeignKey("AdminRankId") + .OnDelete(DeleteBehavior.SetNull); + }); + + modelBuilder.Entity("Content.Server.Database.AdminFlag", b => + { + b.HasOne("Content.Server.Database.Admin", "Admin") + .WithMany("Flags") + .HasForeignKey("AdminId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Content.Server.Database.AdminRankFlag", b => + { + b.HasOne("Content.Server.Database.AdminRank", "Rank") + .WithMany("Flags") + .HasForeignKey("AdminRankId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Content.Server.Database.Antag", b => + { + b.HasOne("Content.Server.Database.Profile", "Profile") + .WithMany("Antags") + .HasForeignKey("ProfileId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Content.Server.Database.Job", b => + { + b.HasOne("Content.Server.Database.Profile", "Profile") + .WithMany("Jobs") + .HasForeignKey("ProfileId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Content.Server.Database.PostgresServerUnban", b => + { + b.HasOne("Content.Server.Database.PostgresServerBan", "Ban") + .WithOne("Unban") + .HasForeignKey("Content.Server.Database.PostgresServerUnban", "BanId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Content.Server.Database.Profile", b => + { + b.HasOne("Content.Server.Database.Preference", "Preference") + .WithMany("Profiles") + .HasForeignKey("PreferenceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Content.Server.Database/Migrations/Postgres/20201109092921_ExtraIndices.cs b/Content.Server.Database/Migrations/Postgres/20201109092921_ExtraIndices.cs new file mode 100644 index 0000000000..5521f82d27 --- /dev/null +++ b/Content.Server.Database/Migrations/Postgres/20201109092921_ExtraIndices.cs @@ -0,0 +1,42 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +namespace Content.Server.Database.Migrations.Postgres +{ + public partial class ExtraIndices : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateIndex( + name: "IX_player_last_seen_user_name", + table: "player", + column: "last_seen_user_name"); + + migrationBuilder.CreateIndex( + name: "IX_admin_rank_flag_flag_admin_rank_id", + table: "admin_rank_flag", + columns: new[] { "flag", "admin_rank_id" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_admin_flag_flag_admin_id", + table: "admin_flag", + columns: new[] { "flag", "admin_id" }, + unique: true); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropIndex( + name: "IX_player_last_seen_user_name", + table: "player"); + + migrationBuilder.DropIndex( + name: "IX_admin_rank_flag_flag_admin_rank_id", + table: "admin_rank_flag"); + + migrationBuilder.DropIndex( + name: "IX_admin_flag_flag_admin_id", + table: "admin_flag"); + } + } +} diff --git a/Content.Server.Database/Migrations/Postgres/PostgresServerDbContextModelSnapshot.cs b/Content.Server.Database/Migrations/Postgres/PostgresServerDbContextModelSnapshot.cs index 916c5843f2..16f84e3e27 100644 --- a/Content.Server.Database/Migrations/Postgres/PostgresServerDbContextModelSnapshot.cs +++ b/Content.Server.Database/Migrations/Postgres/PostgresServerDbContextModelSnapshot.cs @@ -67,6 +67,9 @@ namespace Content.Server.Database.Migrations.Postgres b.HasIndex("AdminId"); + b.HasIndex("Flag", "AdminId") + .IsUnique(); + b.ToTable("admin_flag"); }); @@ -109,6 +112,9 @@ namespace Content.Server.Database.Migrations.Postgres b.HasIndex("AdminRankId"); + b.HasIndex("Flag", "AdminRankId") + .IsUnique(); + b.ToTable("admin_rank_flag"); }); @@ -260,6 +266,8 @@ namespace Content.Server.Database.Migrations.Postgres b.HasKey("Id"); + b.HasIndex("LastSeenUserName"); + b.HasIndex("UserId") .IsUnique(); diff --git a/Content.Server.Database/Migrations/Sqlite/20201109092917_ExtraIndices.Designer.cs b/Content.Server.Database/Migrations/Sqlite/20201109092917_ExtraIndices.Designer.cs new file mode 100644 index 0000000000..ca7d412b93 --- /dev/null +++ b/Content.Server.Database/Migrations/Sqlite/20201109092917_ExtraIndices.Designer.cs @@ -0,0 +1,484 @@ +// +using System; +using Content.Server.Database; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +namespace Content.Server.Database.Migrations.Sqlite +{ + [DbContext(typeof(SqliteServerDbContext))] + [Migration("20201109092917_ExtraIndices")] + partial class ExtraIndices + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "3.1.4"); + + modelBuilder.Entity("Content.Server.Database.Admin", b => + { + b.Property("UserId") + .ValueGeneratedOnAdd() + .HasColumnName("user_id") + .HasColumnType("TEXT"); + + b.Property("AdminRankId") + .HasColumnName("admin_rank_id") + .HasColumnType("INTEGER"); + + b.Property("Title") + .HasColumnName("title") + .HasColumnType("TEXT"); + + b.HasKey("UserId"); + + b.HasIndex("AdminRankId"); + + b.ToTable("admin"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminFlag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("admin_flag_id") + .HasColumnType("INTEGER"); + + b.Property("AdminId") + .HasColumnName("admin_id") + .HasColumnType("TEXT"); + + b.Property("Flag") + .IsRequired() + .HasColumnName("flag") + .HasColumnType("TEXT"); + + b.Property("Negative") + .HasColumnName("negative") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AdminId"); + + b.HasIndex("Flag", "AdminId") + .IsUnique(); + + b.ToTable("admin_flag"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminRank", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("admin_rank_id") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasColumnName("name") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("admin_rank"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminRankFlag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("admin_rank_flag_id") + .HasColumnType("INTEGER"); + + b.Property("AdminRankId") + .HasColumnName("admin_rank_id") + .HasColumnType("INTEGER"); + + b.Property("Flag") + .IsRequired() + .HasColumnName("flag") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("AdminRankId"); + + b.HasIndex("Flag", "AdminRankId") + .IsUnique(); + + b.ToTable("admin_rank_flag"); + }); + + modelBuilder.Entity("Content.Server.Database.Antag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("antag_id") + .HasColumnType("INTEGER"); + + b.Property("AntagName") + .IsRequired() + .HasColumnName("antag_name") + .HasColumnType("TEXT"); + + b.Property("ProfileId") + .HasColumnName("profile_id") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ProfileId", "AntagName") + .IsUnique(); + + b.ToTable("antag"); + }); + + modelBuilder.Entity("Content.Server.Database.AssignedUserId", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("assigned_user_id_id") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnName("user_id") + .HasColumnType("TEXT"); + + b.Property("UserName") + .IsRequired() + .HasColumnName("user_name") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId") + .IsUnique(); + + b.HasIndex("UserName") + .IsUnique(); + + b.ToTable("assigned_user_id"); + }); + + modelBuilder.Entity("Content.Server.Database.Job", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("job_id") + .HasColumnType("INTEGER"); + + b.Property("JobName") + .IsRequired() + .HasColumnName("job_name") + .HasColumnType("TEXT"); + + b.Property("Priority") + .HasColumnName("priority") + .HasColumnType("INTEGER"); + + b.Property("ProfileId") + .HasColumnName("profile_id") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ProfileId"); + + b.ToTable("job"); + }); + + modelBuilder.Entity("Content.Server.Database.Preference", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("preference_id") + .HasColumnType("INTEGER"); + + b.Property("SelectedCharacterSlot") + .HasColumnName("selected_character_slot") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnName("user_id") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("preference"); + }); + + modelBuilder.Entity("Content.Server.Database.Profile", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("profile_id") + .HasColumnType("INTEGER"); + + b.Property("Age") + .HasColumnName("age") + .HasColumnType("INTEGER"); + + b.Property("CharacterName") + .IsRequired() + .HasColumnName("char_name") + .HasColumnType("TEXT"); + + b.Property("EyeColor") + .IsRequired() + .HasColumnName("eye_color") + .HasColumnType("TEXT"); + + b.Property("FacialHairColor") + .IsRequired() + .HasColumnName("facial_hair_color") + .HasColumnType("TEXT"); + + b.Property("FacialHairName") + .IsRequired() + .HasColumnName("facial_hair_name") + .HasColumnType("TEXT"); + + b.Property("HairColor") + .IsRequired() + .HasColumnName("hair_color") + .HasColumnType("TEXT"); + + b.Property("HairName") + .IsRequired() + .HasColumnName("hair_name") + .HasColumnType("TEXT"); + + b.Property("PreferenceId") + .HasColumnName("preference_id") + .HasColumnType("INTEGER"); + + b.Property("PreferenceUnavailable") + .HasColumnName("pref_unavailable") + .HasColumnType("INTEGER"); + + b.Property("Sex") + .IsRequired() + .HasColumnName("sex") + .HasColumnType("TEXT"); + + b.Property("SkinColor") + .IsRequired() + .HasColumnName("skin_color") + .HasColumnType("TEXT"); + + b.Property("Slot") + .HasColumnName("slot") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("PreferenceId"); + + b.HasIndex("Slot", "PreferenceId") + .IsUnique(); + + b.ToTable("profile"); + }); + + modelBuilder.Entity("Content.Server.Database.SqliteConnectionLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("connection_log_id") + .HasColumnType("INTEGER"); + + b.Property("Address") + .IsRequired() + .HasColumnName("address") + .HasColumnType("TEXT"); + + b.Property("Time") + .HasColumnName("time") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnName("user_id") + .HasColumnType("TEXT"); + + b.Property("UserName") + .IsRequired() + .HasColumnName("user_name") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("connection_log"); + }); + + modelBuilder.Entity("Content.Server.Database.SqlitePlayer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("player_id") + .HasColumnType("INTEGER"); + + b.Property("FirstSeenTime") + .HasColumnName("first_seen_time") + .HasColumnType("TEXT"); + + b.Property("LastSeenAddress") + .IsRequired() + .HasColumnName("last_seen_address") + .HasColumnType("TEXT"); + + b.Property("LastSeenTime") + .HasColumnName("last_seen_time") + .HasColumnType("TEXT"); + + b.Property("LastSeenUserName") + .IsRequired() + .HasColumnName("last_seen_user_name") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnName("user_id") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("LastSeenUserName"); + + b.ToTable("player"); + }); + + modelBuilder.Entity("Content.Server.Database.SqliteServerBan", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("ban_id") + .HasColumnType("INTEGER"); + + b.Property("Address") + .HasColumnName("address") + .HasColumnType("TEXT"); + + b.Property("BanTime") + .HasColumnName("ban_time") + .HasColumnType("TEXT"); + + b.Property("BanningAdmin") + .HasColumnName("banning_admin") + .HasColumnType("TEXT"); + + b.Property("ExpirationTime") + .HasColumnName("expiration_time") + .HasColumnType("TEXT"); + + b.Property("Reason") + .IsRequired() + .HasColumnName("reason") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnName("user_id") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("ban"); + }); + + modelBuilder.Entity("Content.Server.Database.SqliteServerUnban", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("unban_id") + .HasColumnType("INTEGER"); + + b.Property("BanId") + .HasColumnName("ban_id") + .HasColumnType("INTEGER"); + + b.Property("UnbanTime") + .HasColumnName("unban_time") + .HasColumnType("TEXT"); + + b.Property("UnbanningAdmin") + .HasColumnName("unbanning_admin") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("BanId") + .IsUnique(); + + b.ToTable("unban"); + }); + + modelBuilder.Entity("Content.Server.Database.Admin", b => + { + b.HasOne("Content.Server.Database.AdminRank", "AdminRank") + .WithMany("Admins") + .HasForeignKey("AdminRankId") + .OnDelete(DeleteBehavior.SetNull); + }); + + modelBuilder.Entity("Content.Server.Database.AdminFlag", b => + { + b.HasOne("Content.Server.Database.Admin", "Admin") + .WithMany("Flags") + .HasForeignKey("AdminId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Content.Server.Database.AdminRankFlag", b => + { + b.HasOne("Content.Server.Database.AdminRank", "Rank") + .WithMany("Flags") + .HasForeignKey("AdminRankId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Content.Server.Database.Antag", b => + { + b.HasOne("Content.Server.Database.Profile", "Profile") + .WithMany("Antags") + .HasForeignKey("ProfileId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Content.Server.Database.Job", b => + { + b.HasOne("Content.Server.Database.Profile", "Profile") + .WithMany("Jobs") + .HasForeignKey("ProfileId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Content.Server.Database.Profile", b => + { + b.HasOne("Content.Server.Database.Preference", "Preference") + .WithMany("Profiles") + .HasForeignKey("PreferenceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Content.Server.Database.SqliteServerUnban", b => + { + b.HasOne("Content.Server.Database.SqliteServerBan", "Ban") + .WithOne("Unban") + .HasForeignKey("Content.Server.Database.SqliteServerUnban", "BanId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Content.Server.Database/Migrations/Sqlite/20201109092917_ExtraIndices.cs b/Content.Server.Database/Migrations/Sqlite/20201109092917_ExtraIndices.cs new file mode 100644 index 0000000000..0c7368a51a --- /dev/null +++ b/Content.Server.Database/Migrations/Sqlite/20201109092917_ExtraIndices.cs @@ -0,0 +1,42 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +namespace Content.Server.Database.Migrations.Sqlite +{ + public partial class ExtraIndices : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateIndex( + name: "IX_player_last_seen_user_name", + table: "player", + column: "last_seen_user_name"); + + migrationBuilder.CreateIndex( + name: "IX_admin_rank_flag_flag_admin_rank_id", + table: "admin_rank_flag", + columns: new[] { "flag", "admin_rank_id" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_admin_flag_flag_admin_id", + table: "admin_flag", + columns: new[] { "flag", "admin_id" }, + unique: true); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropIndex( + name: "IX_player_last_seen_user_name", + table: "player"); + + migrationBuilder.DropIndex( + name: "IX_admin_rank_flag_flag_admin_rank_id", + table: "admin_rank_flag"); + + migrationBuilder.DropIndex( + name: "IX_admin_flag_flag_admin_id", + table: "admin_flag"); + } + } +} diff --git a/Content.Server.Database/Migrations/Sqlite/SqliteServerDbContextModelSnapshot.cs b/Content.Server.Database/Migrations/Sqlite/SqliteServerDbContextModelSnapshot.cs index 67f92bea49..bf24a18637 100644 --- a/Content.Server.Database/Migrations/Sqlite/SqliteServerDbContextModelSnapshot.cs +++ b/Content.Server.Database/Migrations/Sqlite/SqliteServerDbContextModelSnapshot.cs @@ -62,6 +62,9 @@ namespace Content.Server.Database.Migrations.Sqlite b.HasIndex("AdminId"); + b.HasIndex("Flag", "AdminId") + .IsUnique(); + b.ToTable("admin_flag"); }); @@ -102,6 +105,9 @@ namespace Content.Server.Database.Migrations.Sqlite b.HasIndex("AdminRankId"); + b.HasIndex("Flag", "AdminRankId") + .IsUnique(); + b.ToTable("admin_rank_flag"); }); @@ -340,6 +346,8 @@ namespace Content.Server.Database.Migrations.Sqlite b.HasKey("Id"); + b.HasIndex("LastSeenUserName"); + b.ToTable("player"); }); diff --git a/Content.Server.Database/Model.cs b/Content.Server.Database/Model.cs index d701c5cfcc..c52a41fc0c 100644 --- a/Content.Server.Database/Model.cs +++ b/Content.Server.Database/Model.cs @@ -57,6 +57,14 @@ namespace Content.Server.Database .HasOne(p => p.AdminRank) .WithMany(p => p!.Admins) .OnDelete(DeleteBehavior.SetNull); + + modelBuilder.Entity() + .HasIndex(f => new {f.Flag, f.AdminId}) + .IsUnique(); + + modelBuilder.Entity() + .HasIndex(f => new {f.Flag, f.AdminRankId}) + .IsUnique(); } } diff --git a/Content.Server.Database/ModelPostgres.cs b/Content.Server.Database/ModelPostgres.cs index 998c517c51..400768227c 100644 --- a/Content.Server.Database/ModelPostgres.cs +++ b/Content.Server.Database/ModelPostgres.cs @@ -65,6 +65,9 @@ namespace Content.Server.Database .HasCheckConstraint("LastSeenAddressNotIPv6MappedIPv4", "NOT inet '::ffff:0.0.0.0/96' >>= last_seen_address"); + modelBuilder.Entity() + .HasIndex(p => p.LastSeenUserName); + modelBuilder.Entity() .HasIndex(p => p.UserId); diff --git a/Content.Server.Database/ModelSqlite.cs b/Content.Server.Database/ModelSqlite.cs index 0a7ca69e48..4032bb2b28 100644 --- a/Content.Server.Database/ModelSqlite.cs +++ b/Content.Server.Database/ModelSqlite.cs @@ -21,6 +21,14 @@ namespace Content.Server.Database options.UseSqlite("dummy connection string"); } + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + + modelBuilder.Entity() + .HasIndex(p => p.LastSeenUserName); + } + public SqliteServerDbContext(DbContextOptions options) : base(options) { } diff --git a/Content.Server/Administration/AdminManager.cs b/Content.Server/Administration/AdminManager.cs index 28c5f93c30..8048346941 100644 --- a/Content.Server/Administration/AdminManager.cs +++ b/Content.Server/Administration/AdminManager.cs @@ -4,6 +4,7 @@ using System.IO; using System.Linq; using System.Net; using System.Reflection; +using System.Threading.Tasks; using Content.Server.Database; using Content.Server.Interfaces.Chat; using Content.Server.Players; @@ -40,6 +41,8 @@ namespace Content.Server.Administration private readonly Dictionary _admins = new Dictionary(); + public event Action? OnPermsChanged; + public IEnumerable ActiveAdmins => _admins .Where(p => p.Value.Data.Active) .Select(p => p.Key); @@ -78,6 +81,7 @@ namespace Content.Server.Administration plyData.ExplicitlyDeadminned = true; reg.Data.Active = false; + SendPermsChangedEvent(session); UpdateAdminStatus(session); } @@ -96,9 +100,70 @@ namespace Content.Server.Administration _chat.SendAdminAnnouncement(Loc.GetString("{0} re-adminned themselves.", session.Name)); + SendPermsChangedEvent(session); UpdateAdminStatus(session); } + public async void ReloadAdmin(IPlayerSession player) + { + var data = await LoadAdminData(player); + var curAdmin = _admins.GetValueOrDefault(player); + + if (data == null && curAdmin == null) + { + // Wasn't admin before or after. + return; + } + + if (data == null) + { + // No longer admin. + _admins.Remove(player); + _chat.DispatchServerMessage(player, Loc.GetString("You are no longer an admin.")); + } + else + { + var (aData, rankId, special) = data.Value; + + if (curAdmin == null) + { + // Now an admin. + var reg = new AdminReg(player, aData) + { + IsSpecialLogin = special, + RankId = rankId + }; + _admins.Add(player, reg); + _chat.DispatchServerMessage(player, Loc.GetString("You are now an admin.")); + } + else + { + // Perms changed. + curAdmin.IsSpecialLogin = special; + curAdmin.RankId = rankId; + curAdmin.Data = aData; + } + + if (!player.ContentData()!.ExplicitlyDeadminned) + { + aData.Active = true; + + _chat.DispatchServerMessage(player, Loc.GetString("Your admin permissions have been updated.")); + } + } + + SendPermsChangedEvent(player); + UpdateAdminStatus(player); + } + + public void ReloadAdminsWithRank(int rankId) + { + foreach (var dat in _admins.Values.Where(p => p.RankId == rankId).ToArray()) + { + ReloadAdmin(dat.Session); + } + } + public void Initialize() { _netMgr.RegisterNetMessage(MsgUpdateAdminStatus.NAME); @@ -143,7 +208,7 @@ namespace Content.Server.Administration { if (!_adminCommands.TryGetValue(cmd, out var exFlags)) { - _adminCommands.Add(cmd, new []{flags}); + _adminCommands.Add(cmd, new[] {flags}); } else { @@ -213,7 +278,39 @@ namespace Content.Server.Administration private async void LoginAdminMaybe(IPlayerSession session) { - AdminReg reg; + var adminDat = await LoadAdminData(session); + if (adminDat == null) + { + // Not an admin. + return; + } + + var (dat, rankId, specialLogin) = adminDat.Value; + var reg = new AdminReg(session, dat) + { + IsSpecialLogin = specialLogin, + RankId = rankId + }; + + _admins.Add(session, reg); + + if (!session.ContentData()!.ExplicitlyDeadminned) + { + reg.Data.Active = true; + + if (_cfg.GetCVar(CCVars.AdminAnnounceLogin)) + { + _chat.SendAdminAnnouncement(Loc.GetString("Admin login: {0}", session.Name)); + } + + SendPermsChangedEvent(session); + } + + UpdateAdminStatus(session); + } + + private async Task<(AdminData dat, int? rankId, bool specialLogin)?> LoadAdminData(IPlayerSession session) + { if (IsLocal(session) && _cfg.GetCVar(CCVars.ConsoleLoginLocal)) { var data = new AdminData @@ -222,10 +319,7 @@ namespace Content.Server.Administration Flags = AdminFlagsExt.Everything, }; - reg = new AdminReg(session, data) - { - IsSpecialLogin = true, - }; + return (data, null, true); } else { @@ -234,7 +328,7 @@ namespace Content.Server.Administration if (dbData == null) { // Not an admin! - return; + return null; } var flags = AdminFlags.None; @@ -271,22 +365,8 @@ namespace Content.Server.Administration data.Title = dbData.AdminRank.Name; } - reg = new AdminReg(session, data); + return (data, dbData.AdminRankId, false); } - - _admins.Add(session, reg); - - if (!session.ContentData()!.ExplicitlyDeadminned) - { - reg.Data.Active = true; - - if (_cfg.GetCVar(CCVars.AdminAnnounceLogin)) - { - _chat.SendAdminAnnouncement(Loc.GetString("Admin login: {0}", session.Name)); - } - } - - UpdateAdminStatus(session); } private static bool IsLocal(IPlayerSession player) @@ -372,14 +452,20 @@ namespace Content.Server.Administration return GetAdminData(session)?.CanAdminMenu() ?? false; } + private void SendPermsChangedEvent(IPlayerSession session) + { + var flags = GetAdminData(session)?.Flags; + OnPermsChanged?.Invoke(new AdminPermsChangedEventArgs(session, flags)); + } + private sealed class AdminReg { public IPlayerSession Session; public AdminData Data; + public int? RankId; // Such as console.loginlocal - // Means that stuff like permissions editing is blocked. public bool IsSpecialLogin; public AdminReg(IPlayerSession session, AdminData data) diff --git a/Content.Server/Administration/AdminPermsChangedEventArgs.cs b/Content.Server/Administration/AdminPermsChangedEventArgs.cs new file mode 100644 index 0000000000..de2a7b0dbd --- /dev/null +++ b/Content.Server/Administration/AdminPermsChangedEventArgs.cs @@ -0,0 +1,33 @@ +using System; +using Content.Shared.Administration; +using Robust.Server.Interfaces.Player; + +namespace Content.Server.Administration +{ + /// + /// Sealed when the permissions of an admin on the server change. + /// + public sealed class AdminPermsChangedEventArgs : EventArgs + { + public AdminPermsChangedEventArgs(IPlayerSession player, AdminFlags? flags) + { + Player = player; + Flags = flags; + } + + /// + /// The player that had their admin permissions changed. + /// + public IPlayerSession Player { get; } + + /// + /// The admin flags of the player. Null if the player is no longer an admin. + /// + public AdminFlags? Flags { get; } + + /// + /// Whether the player is now an admin. + /// + public bool IsAdmin => Flags.HasValue; + } +} diff --git a/Content.Server/Administration/Commands/OpenPermissionsCommand.cs b/Content.Server/Administration/Commands/OpenPermissionsCommand.cs new file mode 100644 index 0000000000..093a10733a --- /dev/null +++ b/Content.Server/Administration/Commands/OpenPermissionsCommand.cs @@ -0,0 +1,31 @@ +using Content.Server.Eui; +using Content.Shared.Administration; +using Robust.Server.Interfaces.Console; +using Robust.Server.Interfaces.Player; +using Robust.Shared.IoC; + +#nullable enable + +namespace Content.Server.Administration.Commands +{ + [AdminCommand(AdminFlags.Permissions)] + public sealed class OpenPermissionsCommand : IClientCommand + { + public string Command => "permissions"; + public string Description => "Opens the admin permissions panel."; + public string Help => "Usage: permissions"; + + public void Execute(IConsoleShell shell, IPlayerSession? player, string[] args) + { + if (player == null) + { + shell.SendText(player, "This does not work from the server console."); + return; + } + + var eui = IoCManager.Resolve(); + var ui = new PermissionsEui(); + eui.OpenEui(ui, player); + } + } +} diff --git a/Content.Server/Administration/IAdminManager.cs b/Content.Server/Administration/IAdminManager.cs index e475ebeec8..ba2088f0b6 100644 --- a/Content.Server/Administration/IAdminManager.cs +++ b/Content.Server/Administration/IAdminManager.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using Content.Shared.Administration; using Robust.Server.Interfaces.Player; @@ -11,6 +12,11 @@ namespace Content.Server.Administration /// public interface IAdminManager { + /// + /// Fired when the permissions of an admin on the server changed. + /// + event Action OnPermsChanged; + /// /// Gets all active admins currently on the server. /// @@ -29,6 +35,16 @@ namespace Content.Server.Administration /// if the player is not an admin. AdminData? GetAdminData(IPlayerSession session, bool includeDeAdmin = false); + /// + /// See if a player has an admin flag. + /// + /// True if the player is and admin and has the specified flags. + bool HasAdminFlag(IPlayerSession player, AdminFlags flag) + { + var data = GetAdminData(player); + return data != null && data.HasFlag(flag); + } + /// /// De-admins an admin temporarily so they are effectively a normal player. /// @@ -42,6 +58,19 @@ namespace Content.Server.Administration /// void ReAdmin(IPlayerSession session); + /// + /// Re-loads the permissions of an player in case their admin data changed DB-side. + /// + /// + void ReloadAdmin(IPlayerSession player); + + /// + /// Reloads admin permissions for all admins with a certain rank. + /// + /// The database ID of the rank. + /// + void ReloadAdminsWithRank(int rankId); + void Initialize(); } } diff --git a/Content.Server/Administration/PermissionsEui.cs b/Content.Server/Administration/PermissionsEui.cs new file mode 100644 index 0000000000..018b3a83ab --- /dev/null +++ b/Content.Server/Administration/PermissionsEui.cs @@ -0,0 +1,460 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Content.Server.Database; +using Content.Server.Eui; +using Content.Shared.Administration; +using Content.Shared.Eui; +using Robust.Server.Interfaces.Player; +using Robust.Shared.IoC; +using Robust.Shared.Log; +using Robust.Shared.Network; +using DbAdminRank = Content.Server.Database.AdminRank; +using static Content.Shared.Administration.PermissionsEuiMsg; + +#nullable enable + +namespace Content.Server.Administration +{ + public sealed class PermissionsEui : BaseEui + { + [Dependency] private readonly IPlayerManager _playerManager = default!; + [Dependency] private readonly IServerDbManager _db = default!; + [Dependency] private readonly IAdminManager _adminManager = default!; + + private bool _isLoading; + + private readonly List<(Admin a, string? lastUserName)> _admins = new List<(Admin, string? lastUserName)>(); + private readonly List _adminRanks = new List(); + + public PermissionsEui() + { + IoCManager.InjectDependencies(this); + } + + public override void Opened() + { + base.Opened(); + + StateDirty(); + LoadFromDb(); + _adminManager.OnPermsChanged += AdminManagerOnOnPermsChanged; + } + + public override void Closed() + { + base.Closed(); + + _adminManager.OnPermsChanged -= AdminManagerOnOnPermsChanged; + } + + private void AdminManagerOnOnPermsChanged(AdminPermsChangedEventArgs obj) + { + // Close UI if user loses +PERMISSIONS. + if (obj.Player == Player && !UserAdminFlagCheck(AdminFlags.Permissions)) + { + Close(); + } + } + + public override EuiStateBase GetNewState() + { + if (_isLoading) + { + return new PermissionsEuiState + { + IsLoading = true + }; + } + + return new PermissionsEuiState + { + Admins = _admins.Select(p => new PermissionsEuiState.AdminData + { + PosFlags = AdminFlagsExt.NamesToFlags(p.a.Flags.Where(f => !f.Negative).Select(f => f.Flag)), + NegFlags = AdminFlagsExt.NamesToFlags(p.a.Flags.Where(f => f.Negative).Select(f => f.Flag)), + Title = p.a.Title, + RankId = p.a.AdminRankId, + UserId = new NetUserId(p.a.UserId), + UserName = p.lastUserName + }).ToArray(), + + AdminRanks = _adminRanks.ToDictionary(a => a.Id, a => new PermissionsEuiState.AdminRankData + { + Flags = AdminFlagsExt.NamesToFlags(a.Flags.Select(p => p.Flag)), + Name = a.Name + }) + }; + } + + public override async void HandleMessage(EuiMessageBase msg) + { + switch (msg) + { + case Close _: + { + Close(); + break; + } + + case AddAdmin ca: + { + await HandleCreateAdmin(ca); + break; + } + + case UpdateAdmin ua: + { + await HandleUpdateAdmin(ua); + break; + } + + case RemoveAdmin ra: + { + await HandleRemoveAdmin(ra); + break; + } + + case AddAdminRank ar: + { + await HandleAddAdminRank(ar); + break; + } + + case UpdateAdminRank ur: + { + await HandleUpdateAdminRank(ur); + break; + } + + case RemoveAdminRank ra: + { + await HandleRemoveAdminRank(ra); + break; + } + } + + if (!IsShutDown) + { + LoadFromDb(); + } + } + + private async Task HandleRemoveAdminRank(RemoveAdminRank rr) + { + var rank = await _db.GetAdminRankAsync(rr.Id); + if (rank == null) + { + return; + } + + if (!CanTouchRank(rank)) + { + Logger.WarningS("admin.perms", $"{Player} tried to remove higher-ranked admin rank {rank.Name}"); + return; + } + + await _db.RemoveAdminRankAsync(rr.Id); + + _adminManager.ReloadAdminsWithRank(rr.Id); + } + + private async Task HandleUpdateAdminRank(UpdateAdminRank ur) + { + var rank = await _db.GetAdminRankAsync(ur.Id); + if (rank == null) + { + return; + } + + if (!CanTouchRank(rank)) + { + Logger.WarningS("admin.perms", $"{Player} tried to update higher-ranked admin rank {rank.Name}"); + return; + } + + if (!UserAdminFlagCheck(ur.Flags)) + { + Logger.WarningS("admin.perms", $"{Player} tried to give a rank permissions above their authorization."); + return; + } + + rank.Flags = GenRankFlagList(ur.Flags); + rank.Name = ur.Name; + + await _db.UpdateAdminRankAsync(rank); + + var flagText = string.Join(' ', AdminFlagsExt.FlagsToNames(ur.Flags).Select(f => $"+{f}")); + Logger.InfoS("admin.perms", $"{Player} updated admin rank {rank.Name}/{flagText}."); + + _adminManager.ReloadAdminsWithRank(ur.Id); + } + + private async Task HandleAddAdminRank(AddAdminRank ar) + { + if (!UserAdminFlagCheck(ar.Flags)) + { + Logger.WarningS("admin.perms", $"{Player} tried to give a rank permissions above their authorization."); + return; + } + + var rank = new DbAdminRank + { + Name = ar.Name, + Flags = GenRankFlagList(ar.Flags) + }; + + await _db.AddAdminRankAsync(rank); + + var flagText = string.Join(' ', AdminFlagsExt.FlagsToNames(ar.Flags).Select(f => $"+{f}")); + Logger.InfoS("admin.perms", $"{Player} added admin rank {rank.Name}/{flagText}."); + } + + private async Task HandleRemoveAdmin(RemoveAdmin ra) + { + var admin = await _db.GetAdminDataForAsync(ra.UserId); + if (admin == null) + { + // Doesn't exist. + return; + } + + if (!CanTouchAdmin(admin)) + { + Logger.WarningS("admin.perms", $"{Player} tried to remove higher-ranked admin {ra.UserId.ToString()}"); + return; + } + + await _db.RemoveAdminAsync(ra.UserId); + + var record = await _db.GetPlayerRecordByUserId(ra.UserId); + Logger.InfoS("admin.perms", $"{Player} removed admin {record?.LastSeenUserName ?? ra.UserId.ToString()}"); + + if (_playerManager.TryGetSessionById(ra.UserId, out var player)) + { + _adminManager.ReloadAdmin(player); + } + } + + private async Task HandleUpdateAdmin(UpdateAdmin ua) + { + if (!CheckCreatePerms(ua.PosFlags, ua.NegFlags)) + { + return; + } + + var admin = await _db.GetAdminDataForAsync(ua.UserId); + if (admin == null) + { + // Was removed in the mean time I guess? + return; + } + + if (!CanTouchAdmin(admin)) + { + Logger.WarningS("admin.perms", $"{Player} tried to modify higher-ranked admin {ua.UserId.ToString()}"); + return; + } + + admin.Title = ua.Title; + admin.AdminRankId = ua.RankId; + admin.Flags = GenAdminFlagList(ua.PosFlags, ua.NegFlags); + + await _db.UpdateAdminAsync(admin); + + var playerRecord = await _db.GetPlayerRecordByUserId(ua.UserId); + var (bad, rankName) = await FetchAndCheckRank(ua.RankId); + if (bad) + { + return; + } + + var name = playerRecord?.LastSeenUserName ?? ua.UserId.ToString(); + var title = ua.Title ?? ""; + var flags = AdminFlagsExt.PosNegFlagsText(ua.PosFlags, ua.NegFlags); + + Logger.InfoS("admin.perms", $"{Player} updated admin {name} to {title}/{rankName}/{flags}"); + + if (_playerManager.TryGetSessionById(ua.UserId, out var player)) + { + _adminManager.ReloadAdmin(player); + } + } + + private async Task HandleCreateAdmin(AddAdmin ca) + { + if (!CheckCreatePerms(ca.PosFlags, ca.NegFlags)) + { + return; + } + + string name; + NetUserId userId; + if (Guid.TryParse(ca.UserNameOrId, out var guid)) + { + userId = new NetUserId(guid); + var playerRecord = await _db.GetPlayerRecordByUserId(userId); + if (playerRecord == null) + { + name = userId.ToString(); + } + else + { + name = playerRecord.LastSeenUserName; + } + } + else + { + // Username entered, resolve user ID from DB. + var dbPlayer = await _db.GetPlayerRecordByUserName(ca.UserNameOrId); + if (dbPlayer == null) + { + // username not in DB. + // TODO: Notify user. + Logger.WarningS("admin.perms", + $"{Player} tried to add admin with unknown username {ca.UserNameOrId}."); + return; + } + + userId = dbPlayer.UserId; + name = ca.UserNameOrId; + } + + var existing = await _db.GetAdminDataForAsync(userId); + if (existing != null) + { + // Already exists. + return; + } + + var (bad, rankName) = await FetchAndCheckRank(ca.RankId); + if (bad) + { + return; + } + + rankName ??= ""; + + var admin = new Admin + { + Flags = GenAdminFlagList(ca.PosFlags, ca.NegFlags), + AdminRankId = ca.RankId, + UserId = userId.UserId, + Title = ca.Title + }; + + await _db.AddAdminAsync(admin); + + var title = ca.Title ?? ""; + var flags = AdminFlagsExt.PosNegFlagsText(ca.PosFlags, ca.NegFlags); + + Logger.InfoS("admin.perms", $"{Player} added admin {name} as {title}/{rankName}/{flags}"); + + if (_playerManager.TryGetSessionById(userId, out var player)) + { + _adminManager.ReloadAdmin(player); + } + } + + // ReSharper disable once ParameterOnlyUsedForPreconditionCheck.Local + private bool CheckCreatePerms(AdminFlags posFlags, AdminFlags negFlags) + { + if ((posFlags & negFlags) != 0) + { + // Can't have overlapping pos and neg flags. + // Just deny the entire message. + return false; + } + + if (!UserAdminFlagCheck(posFlags)) + { + // Can't create an admin with higher perms than yourself, obviously. + Logger.WarningS("admin.perms", $"{Player} tried to grant admin powers above their authorization."); + return false; + } + + return true; + } + + private async Task<(bool bad, string?)> FetchAndCheckRank(int? rankId) + { + string? ret = null; + if (rankId is { } r) + { + var rank = await _db.GetAdminRankAsync(r); + if (rank == null) + { + // Tried to set to nonexistent rank. + Logger.WarningS("admin.perms", $"{Player} tried to assign nonexistent admin rank."); + return (true, null); + } + + ret = rank.Name; + + var rankFlags = AdminFlagsExt.NamesToFlags(rank.Flags.Select(p => p.Flag)); + if (!UserAdminFlagCheck(rankFlags)) + { + // Can't assign a rank with flags you don't have yourself. + Logger.WarningS("admin.perms", $"{Player} tried to assign admin rank above their authorization."); + return (true, null); + } + } + + return (false, ret); + } + + private async void LoadFromDb() + { + StateDirty(); + _isLoading = true; + var (admins, ranks) = await _db.GetAllAdminAndRanksAsync(); + + _admins.Clear(); + _admins.AddRange(admins); + _adminRanks.Clear(); + _adminRanks.AddRange(ranks); + + _isLoading = false; + StateDirty(); + } + + private static List GenAdminFlagList(AdminFlags posFlags, AdminFlags negFlags) + { + var posFlagList = AdminFlagsExt.FlagsToNames(posFlags); + var negFlagList = AdminFlagsExt.FlagsToNames(negFlags); + + return posFlagList + .Select(f => new AdminFlag {Negative = false, Flag = f}) + .Concat(negFlagList.Select(f => new AdminFlag {Negative = true, Flag = f})) + .ToList(); + } + + private static List GenRankFlagList(AdminFlags flags) + { + return AdminFlagsExt.FlagsToNames(flags).Select(f => new AdminRankFlag {Flag = f}).ToList(); + } + + private bool UserAdminFlagCheck(AdminFlags flags) + { + return _adminManager.HasAdminFlag(Player, flags); + } + + private bool CanTouchAdmin(Admin admin) + { + var posFlags = AdminFlagsExt.NamesToFlags(admin.Flags.Where(f => !f.Negative).Select(f => f.Flag)); + var rankFlags = AdminFlagsExt.NamesToFlags( + admin.AdminRank?.Flags.Select(f => f.Flag) ?? Array.Empty()); + + var totalFlags = posFlags | rankFlags; + return UserAdminFlagCheck(totalFlags); + } + + private bool CanTouchRank(DbAdminRank rank) + { + var rankFlags = AdminFlagsExt.NamesToFlags(rank.Flags.Select(f => f.Flag)); + + return UserAdminFlagCheck(rankFlags); + } + } +} diff --git a/Content.Server/Database/PlayerRecord.cs b/Content.Server/Database/PlayerRecord.cs new file mode 100644 index 0000000000..19e05f833c --- /dev/null +++ b/Content.Server/Database/PlayerRecord.cs @@ -0,0 +1,29 @@ +using System; +using System.Net; +using Robust.Shared.Network; + +namespace Content.Server.Database +{ + public sealed class PlayerRecord + { + public NetUserId UserId { get; } + public DateTimeOffset FirstSeenTime { get; } + public string LastSeenUserName { get; } + public DateTimeOffset LastSeenTime { get; } + public IPAddress LastSeenAddress { get; } + + public PlayerRecord( + NetUserId userId, + DateTimeOffset firstSeenTime, + string lastSeenUserName, + DateTimeOffset lastSeenTime, + IPAddress lastSeenAddress) + { + UserId = userId; + FirstSeenTime = firstSeenTime; + LastSeenUserName = lastSeenUserName; + LastSeenTime = lastSeenTime; + LastSeenAddress = lastSeenAddress; + } + } +} diff --git a/Content.Server/Database/ServerDbBase.cs b/Content.Server/Database/ServerDbBase.cs index e09ebc014e..c91f4ffd71 100644 --- a/Content.Server/Database/ServerDbBase.cs +++ b/Content.Server/Database/ServerDbBase.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; using System.Linq; using System.Net; +using System.Threading; using System.Threading.Tasks; using Content.Shared.Preferences; using Microsoft.EntityFrameworkCore; @@ -211,6 +212,8 @@ namespace Content.Server.Database * PLAYER RECORDS */ public abstract Task UpdatePlayerRecord(NetUserId userId, string userName, IPAddress address); + public abstract Task GetPlayerRecordByUserName(string userName, CancellationToken cancel); + public abstract Task GetPlayerRecordByUserId(NetUserId userId, CancellationToken cancel); /* * CONNECTION LOG @@ -220,7 +223,7 @@ namespace Content.Server.Database /* * ADMIN STUFF */ - public async Task GetAdminDataForAsync(NetUserId userId) + public async Task GetAdminDataForAsync(NetUserId userId, CancellationToken cancel) { await using var db = await GetDb(); @@ -228,7 +231,75 @@ namespace Content.Server.Database .Include(p => p.Flags) .Include(p => p.AdminRank) .ThenInclude(p => p!.Flags) - .SingleOrDefaultAsync(p => p.UserId == userId.UserId); + .SingleOrDefaultAsync(p => p.UserId == userId.UserId, cancel); + } + + public abstract Task<((Admin, string? lastUserName)[] admins, AdminRank[])> + GetAllAdminAndRanksAsync(CancellationToken cancel); + + public async Task GetAdminRankDataForAsync(int id, CancellationToken cancel = default) + { + await using var db = await GetDb(); + + return await db.DbContext.AdminRank + .Include(r => r.Flags) + .SingleOrDefaultAsync(r => r.Id == id, cancel); + } + + public async Task RemoveAdminAsync(NetUserId userId, CancellationToken cancel) + { + await using var db = await GetDb(); + + var admin = await db.DbContext.Admin.SingleAsync(a => a.UserId == userId.UserId, cancel); + db.DbContext.Admin.Remove(admin); + + await db.DbContext.SaveChangesAsync(cancel); + } + + public async Task AddAdminAsync(Admin admin, CancellationToken cancel) + { + await using var db = await GetDb(); + + db.DbContext.Admin.Add(admin); + + await db.DbContext.SaveChangesAsync(cancel); + } + + public async Task UpdateAdminAsync(Admin admin, CancellationToken cancel) + { + await using var db = await GetDb(); + + db.DbContext.Admin.Update(admin); + + await db.DbContext.SaveChangesAsync(cancel); + } + + public async Task RemoveAdminRankAsync(int rankId, CancellationToken cancel) + { + await using var db = await GetDb(); + + var admin = await db.DbContext.AdminRank.SingleAsync(a => a.Id == rankId, cancel); + db.DbContext.AdminRank.Remove(admin); + + await db.DbContext.SaveChangesAsync(cancel); + } + + public async Task AddAdminRankAsync(AdminRank rank, CancellationToken cancel) + { + await using var db = await GetDb(); + + db.DbContext.AdminRank.Add(rank); + + await db.DbContext.SaveChangesAsync(cancel); + } + + public async Task UpdateAdminRankAsync(AdminRank rank, CancellationToken cancel) + { + await using var db = await GetDb(); + + db.DbContext.AdminRank.Update(rank); + + await db.DbContext.SaveChangesAsync(cancel); } protected abstract Task GetDb(); diff --git a/Content.Server/Database/ServerDbManager.cs b/Content.Server/Database/ServerDbManager.cs index f7eb67ee09..e5651f12d5 100644 --- a/Content.Server/Database/ServerDbManager.cs +++ b/Content.Server/Database/ServerDbManager.cs @@ -1,6 +1,7 @@ -using System; +using System; using System.IO; using System.Net; +using System.Threading; using System.Threading.Tasks; using Content.Shared; using Content.Shared.Preferences; @@ -27,7 +28,9 @@ namespace Content.Server.Database // Preferences Task InitPrefsAsync(NetUserId userId, ICharacterProfile defaultProfile); Task SaveSelectedCharacterIndexAsync(NetUserId userId, int index); + Task SaveCharacterSlotAsync(NetUserId userId, ICharacterProfile? profile, int slot); + // Single method for two operations for transaction. Task DeleteSlotAndSetSelectedIndex(NetUserId userId, int deleteSlot, int newSlot); Task GetPlayerPreferencesAsync(NetUserId userId); @@ -42,12 +45,26 @@ namespace Content.Server.Database // Player records Task UpdatePlayerRecordAsync(NetUserId userId, string userName, IPAddress address); + Task GetPlayerRecordByUserName(string userName, CancellationToken cancel = default); + Task GetPlayerRecordByUserId(NetUserId userId, CancellationToken cancel = default); // Connection log Task AddConnectionLogAsync(NetUserId userId, string userName, IPAddress address); // Admins - Task GetAdminDataForAsync(NetUserId userId); + Task GetAdminDataForAsync(NetUserId userId, CancellationToken cancel = default); + Task GetAdminRankAsync(int id, CancellationToken cancel = default); + + Task<((Admin, string? lastUserName)[] admins, AdminRank[])> GetAllAdminAndRanksAsync( + CancellationToken cancel = default); + + Task RemoveAdminAsync(NetUserId userId, CancellationToken cancel = default); + Task AddAdminAsync(Admin admin, CancellationToken cancel = default); + Task UpdateAdminAsync(Admin admin, CancellationToken cancel = default); + + Task RemoveAdminRankAsync(int rankId, CancellationToken cancel = default); + Task AddAdminRankAsync(AdminRank rank, CancellationToken cancel = default); + Task UpdateAdminRankAsync(AdminRank rank, CancellationToken cancel = default); } public sealed class ServerDbManager : IServerDbManager @@ -135,14 +152,65 @@ namespace Content.Server.Database return _db.UpdatePlayerRecord(userId, userName, address); } + public Task GetPlayerRecordByUserName(string userName, CancellationToken cancel = default) + { + return _db.GetPlayerRecordByUserName(userName, cancel); + } + + public Task GetPlayerRecordByUserId(NetUserId userId, CancellationToken cancel = default) + { + return _db.GetPlayerRecordByUserId(userId, cancel); + } + public Task AddConnectionLogAsync(NetUserId userId, string userName, IPAddress address) { return _db.AddConnectionLogAsync(userId, userName, address); } - public Task GetAdminDataForAsync(NetUserId userId) + public Task GetAdminDataForAsync(NetUserId userId, CancellationToken cancel = default) { - return _db.GetAdminDataForAsync(userId); + return _db.GetAdminDataForAsync(userId, cancel); + } + + public Task GetAdminRankAsync(int id, CancellationToken cancel = default) + { + return _db.GetAdminRankDataForAsync(id, cancel); + } + + public Task<((Admin, string? lastUserName)[] admins, AdminRank[])> GetAllAdminAndRanksAsync( + CancellationToken cancel = default) + { + return _db.GetAllAdminAndRanksAsync(cancel); + } + + public Task RemoveAdminAsync(NetUserId userId, CancellationToken cancel = default) + { + return _db.RemoveAdminAsync(userId, cancel); + } + + public Task AddAdminAsync(Admin admin, CancellationToken cancel = default) + { + return _db.AddAdminAsync(admin, cancel); + } + + public Task UpdateAdminAsync(Admin admin, CancellationToken cancel = default) + { + return _db.UpdateAdminAsync(admin, cancel); + } + + public Task RemoveAdminRankAsync(int rankId, CancellationToken cancel = default) + { + return _db.RemoveAdminRankAsync(rankId, cancel); + } + + public Task AddAdminRankAsync(AdminRank rank, CancellationToken cancel = default) + { + return _db.AddAdminRankAsync(rank, cancel); + } + + public Task UpdateAdminRankAsync(AdminRank rank, CancellationToken cancel = default) + { + return _db.UpdateAdminRankAsync(rank, cancel); } private DbContextOptions CreatePostgresOptions() diff --git a/Content.Server/Database/ServerDbPostgres.cs b/Content.Server/Database/ServerDbPostgres.cs index b3ac0388cb..b57e557832 100644 --- a/Content.Server/Database/ServerDbPostgres.cs +++ b/Content.Server/Database/ServerDbPostgres.cs @@ -1,6 +1,8 @@ using System; +using System.Data; using System.Linq; using System.Net; +using System.Threading; using System.Threading.Tasks; using Microsoft.EntityFrameworkCore; using Robust.Shared.Network; @@ -138,6 +140,45 @@ namespace Content.Server.Database await db.PgDbContext.SaveChangesAsync(); } + public override async Task GetPlayerRecordByUserName(string userName, CancellationToken cancel) + { + await using var db = await GetDbImpl(); + + // Sort by descending last seen time. + // So if, due to account renames, we have two people with the same username in the DB, + // the most recent one is picked. + var record = await db.PgDbContext.Player + .OrderByDescending(p => p.LastSeenTime) + .FirstOrDefaultAsync(p => p.LastSeenUserName == userName, cancel); + + return MakePlayerRecord(record); + } + + public override async Task GetPlayerRecordByUserId(NetUserId userId, CancellationToken cancel) + { + await using var db = await GetDbImpl(); + + var record = await db.PgDbContext.Player + .SingleOrDefaultAsync(p => p.UserId == userId.UserId, cancel); + + return MakePlayerRecord(record); + } + + private static PlayerRecord? MakePlayerRecord(PostgresPlayer? record) + { + if (record == null) + { + return null; + } + + return new PlayerRecord( + new NetUserId(record.UserId), + new DateTimeOffset(record.FirstSeenTime, TimeSpan.Zero), + record.LastSeenUserName, + new DateTimeOffset(record.LastSeenTime, TimeSpan.Zero), + record.LastSeenAddress); + } + public override async Task AddConnectionLogAsync(NetUserId userId, string userName, IPAddress address) { await using var db = await GetDbImpl(); @@ -153,6 +194,27 @@ namespace Content.Server.Database await db.PgDbContext.SaveChangesAsync(); } + public override async Task<((Admin, string? lastUserName)[] admins, AdminRank[])> + GetAllAdminAndRanksAsync(CancellationToken cancel) + { + await using var db = await GetDbImpl(); + + // Honestly this probably doesn't even matter but whatever. + await using var tx = + await db.DbContext.Database.BeginTransactionAsync(IsolationLevel.RepeatableRead, cancel); + + // Join with the player table to find their last seen username, if they have one. + var admins = await db.PgDbContext.Admin + .Include(a => a.Flags) + .GroupJoin(db.PgDbContext.Player, a => a.UserId, p => p.UserId, (a, grouping) => new {a, grouping}) + .SelectMany(t => t.grouping.DefaultIfEmpty(), (t, p) => new {t.a, p.LastSeenUserName}) + .ToArrayAsync(cancel); + + var adminRanks = await db.DbContext.AdminRank.Include(a => a.Flags).ToArrayAsync(cancel); + + return (admins.Select(p => (p.a, p.LastSeenUserName)).ToArray(), adminRanks)!; + } + private async Task GetDbImpl() { await _dbReadyTask; diff --git a/Content.Server/Database/ServerDbSqlite.cs b/Content.Server/Database/ServerDbSqlite.cs index 9ecefb0fc2..f8c654c95a 100644 --- a/Content.Server/Database/ServerDbSqlite.cs +++ b/Content.Server/Database/ServerDbSqlite.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Net; @@ -105,6 +106,44 @@ namespace Content.Server.Database await db.SqliteDbContext.SaveChangesAsync(); } + public override async Task GetPlayerRecordByUserName(string userName, CancellationToken cancel) + { + await using var db = await GetDbImpl(); + + // Sort by descending last seen time. + // So if due to account renames we have two people with the same username in the DB, + // the most recent one is picked. + var record = await db.SqliteDbContext.Player + .OrderByDescending(p => p.LastSeenTime) + .FirstOrDefaultAsync(p => p.LastSeenUserName == userName, cancel); + + return MakePlayerRecord(record); + } + + public override async Task GetPlayerRecordByUserId(NetUserId userId, CancellationToken cancel) + { + await using var db = await GetDbImpl(); + + var record = await db.SqliteDbContext.Player + .SingleOrDefaultAsync(p => p.UserId == userId.UserId, cancel); + + return MakePlayerRecord(record); + } + + private static PlayerRecord? MakePlayerRecord(SqlitePlayer? record) + { + if (record == null) + { + return null; + } + + return new PlayerRecord( + new NetUserId(record.UserId), + new DateTimeOffset(record.FirstSeenTime, TimeSpan.Zero), + record.LastSeenUserName, + new DateTimeOffset(record.LastSeenTime, TimeSpan.Zero), + IPAddress.Parse(record.LastSeenAddress)); + } private static ServerBanDef? ConvertBan(SqliteServerBan? ban) { if (ban == null) @@ -156,6 +195,21 @@ namespace Content.Server.Database await db.SqliteDbContext.SaveChangesAsync(); } + public override async Task<((Admin, string? lastUserName)[] admins, AdminRank[])> GetAllAdminAndRanksAsync( + CancellationToken cancel) + { + await using var db = await GetDbImpl(); + + var admins = await db.SqliteDbContext.Admin + .Include(a => a.Flags) + .GroupJoin(db.SqliteDbContext.Player, a => a.UserId, p => p.UserId, (a, grouping) => new {a, grouping}) + .SelectMany(t => t.grouping.DefaultIfEmpty(), (t, p) => new {t.a, p.LastSeenUserName}) + .ToArrayAsync(cancel); + + var adminRanks = await db.DbContext.AdminRank.Include(a => a.Flags).ToArrayAsync(cancel); + + return (admins.Select(p => (p.a, p.LastSeenUserName)).ToArray(), adminRanks)!; + } private async Task GetDbImpl() { diff --git a/Content.Server/EntryPoint.cs b/Content.Server/EntryPoint.cs index 3bc16dee3a..6c8e46bf6d 100644 --- a/Content.Server/EntryPoint.cs +++ b/Content.Server/EntryPoint.cs @@ -2,6 +2,7 @@ using Content.Server.AI.Utility.Considerations; using Content.Server.AI.WorldState; using Content.Server.Database; +using Content.Server.Eui; using Content.Server.GameObjects.Components.Mobs.Speech; using Content.Server.GameObjects.Components.NodeContainer.NodeGroups; using Content.Server.Interfaces; @@ -23,6 +24,7 @@ namespace Content.Server public class EntryPoint : GameServer { private IGameTicker _gameTicker; + private EuiManager _euiManager; private StatusShell _statusShell; /// @@ -50,6 +52,7 @@ namespace Content.Server IoCManager.BuildGraph(); _gameTicker = IoCManager.Resolve(); + _euiManager = IoCManager.Resolve(); IoCManager.Resolve().Initialize(); IoCManager.Resolve().Initialize(); @@ -79,6 +82,7 @@ namespace Content.Server IoCManager.Resolve().Initialize(); IoCManager.Resolve().Initialize(); IoCManager.Resolve().Initialize(); + _euiManager.Initialize(); } public override void Update(ModUpdateLevel level, FrameEventArgs frameEventArgs) @@ -92,6 +96,11 @@ namespace Content.Server _gameTicker.Update(frameEventArgs); break; } + case ModUpdateLevel.PostEngine: + { + _euiManager.SendUpdates(); + break; + } } } } diff --git a/Content.Server/Eui/BaseEui.cs b/Content.Server/Eui/BaseEui.cs new file mode 100644 index 0000000000..5b58cc4a4a --- /dev/null +++ b/Content.Server/Eui/BaseEui.cs @@ -0,0 +1,97 @@ +using System; +using Content.Shared.Eui; +using Content.Shared.Network.NetMessages; +using Robust.Server.Interfaces.Player; +using Robust.Shared.Interfaces.Network; +using Robust.Shared.IoC; + +#nullable enable + +namespace Content.Server.Eui +{ + public abstract class BaseEui + { + private bool _isStateDirty = false; + + public bool IsShutDown { get; private set; } + public EuiManager Manager { get; private set; } = default!; + public IPlayerSession Player { get; private set; } = default!; + public uint Id { get; private set; } + + public void Initialize(EuiManager manager, IPlayerSession player, uint id) + { + Manager = manager; + Player = player; + Id = id; + Opened(); + } + + public virtual void Opened() + { + + } + + public virtual void Closed() + { + + } + + public virtual void HandleMessage(EuiMessageBase msg) + { + } + + public void Shutdown() + { + Closed(); + IsShutDown = true; + } + + /// + /// Mark the current UI state as dirty and queue for an update. + /// + public void StateDirty() + { + if (_isStateDirty) + { + return; + } + + _isStateDirty = true; + Manager.QueueStateUpdate(this); + } + + public virtual EuiStateBase GetNewState() + { + throw new NotSupportedException(); + } + + public void Close() + { + Manager.CloseEui(this); + } + + public void DoStateUpdate() + { + _isStateDirty = false; + + var state = GetNewState(); + + var netMgr = IoCManager.Resolve(); + var msg = netMgr.CreateNetMessage(); + msg.Id = Id; + msg.State = state; + + netMgr.ServerSendMessage(msg, Player.ConnectedClient); + } + + public void SendMessage(EuiMessageBase message) + { + var netMgr = IoCManager.Resolve(); + var msg = netMgr.CreateNetMessage(); + msg.Id = Id; + msg.Message = message; + + netMgr.ServerSendMessage(msg, Player.ConnectedClient); + } + } +} diff --git a/Content.Server/Eui/EuiManager.cs b/Content.Server/Eui/EuiManager.cs new file mode 100644 index 0000000000..80e0246d08 --- /dev/null +++ b/Content.Server/Eui/EuiManager.cs @@ -0,0 +1,144 @@ +using System; +using System.Collections.Generic; +using Content.Shared.Network.NetMessages; +using Robust.Server.Interfaces.Player; +using Robust.Server.Player; +using Robust.Shared.Enums; +using Robust.Shared.Interfaces.Network; +using Robust.Shared.IoC; +using Robust.Shared.Log; +using Robust.Shared.Utility; + +#nullable enable + +namespace Content.Server.Eui +{ + public sealed class EuiManager : IPostInjectInit + { + [Dependency] private readonly IPlayerManager _players = default!; + [Dependency] private readonly IServerNetManager _net = default!; + + private readonly Dictionary _playerData = + new Dictionary(); + + private readonly Queue<(IPlayerSession player, uint id)> _stateUpdateQueue = + new Queue<(IPlayerSession, uint id)>(); + + private sealed class PlayerEuiData + { + public uint NextId = 1; + public readonly Dictionary OpenUIs = new Dictionary(); + } + + void IPostInjectInit.PostInject() + { + _players.PlayerStatusChanged += PlayerStatusChanged; + } + + public void Initialize() + { + _net.RegisterNetMessage(MsgEuiCtl.NAME); + _net.RegisterNetMessage(MsgEuiState.NAME); + _net.RegisterNetMessage(MsgEuiMessage.NAME, RxMsgMessage); + } + + public void SendUpdates() + { + while (_stateUpdateQueue.TryDequeue(out var tuple)) + { + var (player, id) = tuple; + + // Check that UI and player still exist. + // COULD have been removed in the mean time. + if (!_playerData.TryGetValue(player, out var plyDat) || !plyDat.OpenUIs.TryGetValue(id, out var ui)) + { + continue; + } + + ui.DoStateUpdate(); + } + } + + public void OpenEui(BaseEui eui, IPlayerSession player) + { + if (eui.Id != 0) + { + throw new ArgumentException("That EUI is already open!"); + } + + var data = _playerData[player]; + var newId = data.NextId++; + eui.Initialize(this, player, newId); + + data.OpenUIs.Add(newId, eui); + + var msg = _net.CreateNetMessage(); + msg.Id = newId; + msg.Type = MsgEuiCtl.CtlType.Open; + msg.OpenType = eui.GetType().Name; + + _net.ServerSendMessage(msg, player.ConnectedClient); + } + + public void CloseEui(BaseEui eui) + { + eui.Closed(); + _playerData[eui.Player].OpenUIs.Remove(eui.Id); + + var msg = _net.CreateNetMessage(); + msg.Id = eui.Id; + msg.Type = MsgEuiCtl.CtlType.Close; + _net.ServerSendMessage(msg, eui.Player.ConnectedClient); + } + + private void RxMsgMessage(MsgEuiMessage message) + { + if (!_players.TryGetSessionByChannel(message.MsgChannel, out var ply)) + { + return; + } + + if (!_playerData.TryGetValue(ply, out var dat)) + { + return; + } + + if (!dat.OpenUIs.TryGetValue(message.Id, out var eui)) + { + Logger.WarningS("eui", $"Got EUI message from player {ply} for non-existing UI {message.Id}"); + return; + } + + eui.HandleMessage(message.Message); + } + + private void PlayerStatusChanged(object? sender, SessionStatusEventArgs e) + { + if (e.NewStatus == SessionStatus.Connected) + { + _playerData.Add(e.Session, new PlayerEuiData()); + } + else if (e.NewStatus == SessionStatus.Disconnected) + { + if (_playerData.TryGetValue(e.Session, out var plyDat)) + { + // Gracefully close all open UIs. + foreach (var ui in plyDat.OpenUIs.Values) + { + ui.Closed(); + } + + _playerData.Remove(e.Session); + } + } + } + + public void QueueStateUpdate(BaseEui eui) + { + DebugTools.Assert(eui.Id != 0, "EUI has not been opened yet."); + DebugTools.Assert(!eui.IsShutDown, "EUI has been closed."); + + _stateUpdateQueue.Enqueue((eui.Player, eui.Id)); + } + } +} diff --git a/Content.Server/ServerContentIoC.cs b/Content.Server/ServerContentIoC.cs index fca5cef686..6d9d94949b 100644 --- a/Content.Server/ServerContentIoC.cs +++ b/Content.Server/ServerContentIoC.cs @@ -4,6 +4,7 @@ using Content.Server.AI.WorldState; using Content.Server.Cargo; using Content.Server.Chat; using Content.Server.Database; +using Content.Server.Eui; using Content.Server.GameObjects.Components.Mobs.Speech; using Content.Server.GameObjects.Components.NodeContainer.NodeGroups; using Content.Server.GameObjects.Components.Power.PowerNetComponents; @@ -48,6 +49,7 @@ namespace Content.Server IoCManager.Register(); IoCManager.Register(); IoCManager.Register(); + IoCManager.Register(); } } } diff --git a/Content.Shared/Administration/AdminFlags.cs b/Content.Shared/Administration/AdminFlags.cs index 4b3a856c55..2f6b336fe6 100644 --- a/Content.Shared/Administration/AdminFlags.cs +++ b/Content.Shared/Administration/AdminFlags.cs @@ -58,7 +58,7 @@ namespace Content.Shared.Administration /// /// Makes you british. /// - Piss = 1 << 9, + //Piss = 1 << 9, /// /// Dangerous host permissions like scsi. diff --git a/Content.Shared/Administration/AdminFlagsExt.cs b/Content.Shared/Administration/AdminFlagsExt.cs index abe198a1f0..0c018e8c3a 100644 --- a/Content.Shared/Administration/AdminFlagsExt.cs +++ b/Content.Shared/Administration/AdminFlagsExt.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Numerics; namespace Content.Shared.Administration @@ -11,10 +12,13 @@ namespace Content.Shared.Administration public static readonly AdminFlags Everything; + public static readonly IReadOnlyList AllFlags; + static AdminFlagsExt() { var t = typeof(AdminFlags); var flags = (AdminFlags[]) Enum.GetValues(t); + var allFlags = new List(); foreach (var value in flags) { @@ -25,10 +29,13 @@ namespace Content.Shared.Administration continue; } + allFlags.Add(value); Everything |= value; NameFlagsMap.Add(name, value); FlagsNameMap[BitOperations.Log2((uint) value)] = name; } + + AllFlags = allFlags.ToArray(); } public static AdminFlags NamesToFlags(IEnumerable names) @@ -69,5 +76,14 @@ namespace Content.Shared.Administration return array; } + + public static string PosNegFlagsText(AdminFlags posFlags, AdminFlags negFlags) + { + var posFlagNames = FlagsToNames(posFlags).Select(f => (flag: f, fText: $"+{f}")); + var negFlagNames = FlagsToNames(negFlags).Select(f => (flag: f, fText: $"-{f}")); + + var flagsText = string.Join(' ', posFlagNames.Concat(negFlagNames).OrderBy(f => f.flag).Select(p => p.fText)); + return flagsText; + } } } diff --git a/Content.Shared/Administration/PermissionsEuiState.cs b/Content.Shared/Administration/PermissionsEuiState.cs new file mode 100644 index 0000000000..f49778ab60 --- /dev/null +++ b/Content.Shared/Administration/PermissionsEuiState.cs @@ -0,0 +1,92 @@ +using System; +using System.Collections.Generic; +using Content.Shared.Eui; +using Robust.Shared.Network; +using Robust.Shared.Serialization; + +namespace Content.Shared.Administration +{ + [Serializable, NetSerializable] + public sealed class PermissionsEuiState : EuiStateBase + { + public bool IsLoading; + + public AdminData[] Admins; + public Dictionary AdminRanks; + + [Serializable, NetSerializable] + public struct AdminData + { + public NetUserId UserId; + public string UserName; + public string Title; + public AdminFlags PosFlags; + public AdminFlags NegFlags; + public int? RankId; + } + + [Serializable, NetSerializable] + public struct AdminRankData + { + public string Name; + public AdminFlags Flags; + } + } + + public static class PermissionsEuiMsg + { + [Serializable, NetSerializable] + public sealed class Close : EuiMessageBase + { + } + + [Serializable, NetSerializable] + public sealed class AddAdmin : EuiMessageBase + { + public string UserNameOrId; + public string Title; + public AdminFlags PosFlags; + public AdminFlags NegFlags; + public int? RankId; + } + + [Serializable, NetSerializable] + public sealed class RemoveAdmin : EuiMessageBase + { + public NetUserId UserId; + } + + [Serializable, NetSerializable] + public sealed class UpdateAdmin : EuiMessageBase + { + public NetUserId UserId; + public string Title; + public AdminFlags PosFlags; + public AdminFlags NegFlags; + public int? RankId; + } + + + [Serializable, NetSerializable] + public sealed class AddAdminRank : EuiMessageBase + { + public string Name; + public AdminFlags Flags; + } + + [Serializable, NetSerializable] + public sealed class RemoveAdminRank : EuiMessageBase + { + public int Id; + } + + [Serializable, NetSerializable] + public sealed class UpdateAdminRank : EuiMessageBase + { + public int Id; + + public string Name; + public AdminFlags Flags; + } + } +} diff --git a/Content.Shared/Eui/EuiMessageBase.cs b/Content.Shared/Eui/EuiMessageBase.cs new file mode 100644 index 0000000000..5a252572d5 --- /dev/null +++ b/Content.Shared/Eui/EuiMessageBase.cs @@ -0,0 +1,10 @@ +using System; + +namespace Content.Shared.Eui +{ + [Serializable] + public abstract class EuiMessageBase + { + + } +} diff --git a/Content.Shared/Eui/EuiStateBase.cs b/Content.Shared/Eui/EuiStateBase.cs new file mode 100644 index 0000000000..c1fe469c35 --- /dev/null +++ b/Content.Shared/Eui/EuiStateBase.cs @@ -0,0 +1,11 @@ +using System; +using Robust.Shared.Serialization; + +namespace Content.Shared.Eui +{ + [Serializable, NetSerializable] + public abstract class EuiStateBase + { + + } +} diff --git a/Content.Shared/Network/NetMessages/MsgEuiCtl.cs b/Content.Shared/Network/NetMessages/MsgEuiCtl.cs new file mode 100644 index 0000000000..d1b553d18c --- /dev/null +++ b/Content.Shared/Network/NetMessages/MsgEuiCtl.cs @@ -0,0 +1,55 @@ +using Lidgren.Network; +using Robust.Shared.Interfaces.Network; +using Robust.Shared.Network; + +namespace Content.Shared.Network.NetMessages +{ + /// + /// Sent server -> client to signal that the client should open an EUI. + /// + public sealed class MsgEuiCtl : NetMessage + { + #region REQUIRED + + public const MsgGroups GROUP = MsgGroups.Command; + public const string NAME = nameof(MsgEuiCtl); + + public MsgEuiCtl(INetChannel channel) : base(NAME, GROUP) { } + + #endregion + + public CtlType Type; + public string OpenType; + public uint Id; + + public override void ReadFromBuffer(NetIncomingMessage buffer) + { + Id = buffer.ReadUInt32(); + Type = (CtlType) buffer.ReadByte(); + switch (Type) + { + case CtlType.Open: + OpenType = buffer.ReadString(); + break; + } + } + + public override void WriteToBuffer(NetOutgoingMessage buffer) + { + buffer.Write(Id); + buffer.Write((byte) Type); + switch (Type) + { + case CtlType.Open: + buffer.Write(OpenType); + break; + } + } + + public enum CtlType : byte + { + Open, + Close + } + } +} diff --git a/Content.Shared/Network/NetMessages/MsgEuiMessage.cs b/Content.Shared/Network/NetMessages/MsgEuiMessage.cs new file mode 100644 index 0000000000..7fe5912d20 --- /dev/null +++ b/Content.Shared/Network/NetMessages/MsgEuiMessage.cs @@ -0,0 +1,48 @@ +using System; +using System.IO; +using Content.Shared.Eui; +using Lidgren.Network; +using Robust.Shared.Interfaces.Network; +using Robust.Shared.Interfaces.Serialization; +using Robust.Shared.IoC; +using Robust.Shared.Network; + +namespace Content.Shared.Network.NetMessages +{ + public sealed class MsgEuiMessage : NetMessage + { + #region REQUIRED + + public const MsgGroups GROUP = MsgGroups.Command; + public const string NAME = nameof(MsgEuiMessage); + + public MsgEuiMessage(INetChannel channel) : base(NAME, GROUP) { } + + #endregion + + public uint Id; + public EuiMessageBase Message; + + public override void ReadFromBuffer(NetIncomingMessage buffer) + { + Id = buffer.ReadUInt32(); + + var ser = IoCManager.Resolve(); + var len = buffer.ReadVariableInt32(); + var stream = buffer.ReadAlignedMemory(len); + Message = ser.Deserialize(stream); + } + + public override void WriteToBuffer(NetOutgoingMessage buffer) + { + buffer.Write(Id); + var stream = new MemoryStream(); + + var ser = IoCManager.Resolve(); + ser.Serialize(stream, Message); + var length = (int)stream.Length; + buffer.WriteVariableInt32(length); + buffer.Write(stream.GetBuffer().AsSpan(0, length)); + } + } +} diff --git a/Content.Shared/Network/NetMessages/MsgEuiState.cs b/Content.Shared/Network/NetMessages/MsgEuiState.cs new file mode 100644 index 0000000000..044c4c17b3 --- /dev/null +++ b/Content.Shared/Network/NetMessages/MsgEuiState.cs @@ -0,0 +1,48 @@ +using System; +using System.IO; +using Content.Shared.Eui; +using Lidgren.Network; +using Robust.Shared.Interfaces.Network; +using Robust.Shared.Interfaces.Serialization; +using Robust.Shared.IoC; +using Robust.Shared.Network; + +namespace Content.Shared.Network.NetMessages +{ + public sealed class MsgEuiState : NetMessage + { + #region REQUIRED + + public const MsgGroups GROUP = MsgGroups.Command; + public const string NAME = nameof(MsgEuiState); + + public MsgEuiState(INetChannel channel) : base(NAME, GROUP) { } + + #endregion + + public uint Id; + public EuiStateBase State; + + public override void ReadFromBuffer(NetIncomingMessage buffer) + { + Id = buffer.ReadUInt32(); + + var ser = IoCManager.Resolve(); + var len = buffer.ReadVariableInt32(); + var stream = buffer.ReadAlignedMemory(len); + State = ser.Deserialize(stream); + } + + public override void WriteToBuffer(NetOutgoingMessage buffer) + { + buffer.Write(Id); + var stream = new MemoryStream(); + + var ser = IoCManager.Resolve(); + ser.Serialize(stream, State); + var length = (int)stream.Length; + buffer.WriteVariableInt32(length); + buffer.Write(stream.GetBuffer().AsSpan(0, length)); + } + } +} From 7bc93bd2a9c333efb94353ccd33aaca4fd21ae99 Mon Sep 17 00:00:00 2001 From: Pieter-Jan Briers Date: Tue, 10 Nov 2020 17:06:20 +0100 Subject: [PATCH 09/11] Missed these in the merge, whoops. --- Content.Server/Commands/Hungry.cs | 3 +++ .../GameObjects/Components/Mobs/ServerAlertsComponent.cs | 4 ++++ 2 files changed, 7 insertions(+) diff --git a/Content.Server/Commands/Hungry.cs b/Content.Server/Commands/Hungry.cs index b3a56c0171..39daf0caf9 100644 --- a/Content.Server/Commands/Hungry.cs +++ b/Content.Server/Commands/Hungry.cs @@ -1,11 +1,14 @@ #nullable enable +using Content.Server.Administration; using Content.Server.GameObjects.Components.Nutrition; +using Content.Shared.Administration; using Content.Shared.GameObjects.Components.Nutrition; using Robust.Server.Interfaces.Console; using Robust.Server.Interfaces.Player; namespace Content.Server.Commands { + [AdminCommand(AdminFlags.Debug)] public class Hungry : IClientCommand { public string Command => "hungry"; diff --git a/Content.Server/GameObjects/Components/Mobs/ServerAlertsComponent.cs b/Content.Server/GameObjects/Components/Mobs/ServerAlertsComponent.cs index 2587b491e4..37b1465891 100644 --- a/Content.Server/GameObjects/Components/Mobs/ServerAlertsComponent.cs +++ b/Content.Server/GameObjects/Components/Mobs/ServerAlertsComponent.cs @@ -1,6 +1,8 @@ using System; +using Content.Server.Administration; using Content.Server.Commands; using Content.Server.GameObjects.EntitySystems; +using Content.Shared.Administration; using Content.Shared.Alert; using Content.Shared.GameObjects.Components.Mobs; using Robust.Server.Interfaces.Console; @@ -88,6 +90,7 @@ namespace Content.Server.GameObjects.Components.Mobs } } + [AdminCommand(AdminFlags.Debug)] public sealed class ShowAlert : IClientCommand { public string Command => "showalert"; @@ -129,6 +132,7 @@ namespace Content.Server.GameObjects.Components.Mobs } } + [AdminCommand(AdminFlags.Debug)] public sealed class ClearAlert : IClientCommand { public string Command => "clearalert"; From 736f9958cc1346e9aa705720bf54e30c3320faf6 Mon Sep 17 00:00:00 2001 From: Pieter-Jan Briers Date: Tue, 10 Nov 2020 21:30:20 +0100 Subject: [PATCH 10/11] Cleanup, code review, comments. --- .../Administration/ClientAdminManager.cs | 6 +-- .../Administration/IClientAdminManager.cs | 35 ++++++++++++++++- Content.Client/Chat/ChatBox.cs | 7 +--- .../Permissions/PermissionsEui.cs | 10 ++--- Content.Server/Administration/AdminManager.cs | 10 ++--- .../Administration/Commands/DeAdminCommand.cs | 2 +- .../Administration/Commands/ReAdminCommand.cs | 2 +- .../Administration/PermissionsEui.cs | 34 ++++++++-------- Content.Server/Chat/ChatManager.cs | 1 - Content.Shared/Administration/AdminData.cs | 33 +++++++++++++++- .../{AdminFlagsExt.cs => AdminFlagsHelper.cs} | 39 ++++++++++++++++++- .../Administration/AdminFlagsExtTest.cs | 4 +- 12 files changed, 136 insertions(+), 47 deletions(-) rename Content.Shared/Administration/{AdminFlagsExt.cs => AdminFlagsHelper.cs} (63%) diff --git a/Content.Client/Administration/ClientAdminManager.cs b/Content.Client/Administration/ClientAdminManager.cs index 96b644579c..4301bfe8d0 100644 --- a/Content.Client/Administration/ClientAdminManager.cs +++ b/Content.Client/Administration/ClientAdminManager.cs @@ -21,8 +21,6 @@ namespace Content.Client.Administration public event Action? AdminStatusUpdated; - public AdminFlags? Flags => _adminData?.Flags; - public bool HasFlag(AdminFlags flag) { return _adminData?.HasFlag(flag) ?? false; @@ -67,12 +65,12 @@ namespace Content.Client.Administration _adminData = message.Admin; if (_adminData != null) { - var flagsText = string.Join("|", AdminFlagsExt.FlagsToNames(_adminData.Flags)); + var flagsText = string.Join("|", AdminFlagsHelper.FlagsToNames(_adminData.Flags)); Logger.InfoS("admin", $"Updated admin status: {_adminData.Active}/{_adminData.Title}/{flagsText}"); } else { - Logger.InfoS("admin", $"Updated admin status: Not admin"); + Logger.InfoS("admin", "Updated admin status: Not admin"); } AdminStatusUpdated?.Invoke(); diff --git a/Content.Client/Administration/IClientAdminManager.cs b/Content.Client/Administration/IClientAdminManager.cs index 1bdf0cb029..e675e3f378 100644 --- a/Content.Client/Administration/IClientAdminManager.cs +++ b/Content.Client/Administration/IClientAdminManager.cs @@ -1,19 +1,50 @@ using System; using Content.Shared.Administration; +#nullable enable + namespace Content.Client.Administration { + /// + /// Manages server admin permissions for the local player. + /// public interface IClientAdminManager { - public event Action AdminStatusUpdated; + /// + /// Fired when the admin status of the local player changes, such as losing admin privileges. + /// + event Action AdminStatusUpdated; - AdminFlags? Flags { get; } + /// + /// Checks whether the local player has an admin flag. + /// + /// The flags to check. Multiple flags can be specified, they must all be held. + /// False if the local player is not an admin, inactive, or does not have all the flags specified. bool HasFlag(AdminFlags flag); + /// + /// Check if a player can execute a specified console command. + /// bool CanCommand(string cmdName); + + /// + /// Check if the local player can open the VV menu. + /// bool CanViewVar(); + + /// + /// Check if the local player can spawn stuff in with the entity/tile spawn panel. + /// bool CanAdminPlace(); + + /// + /// Check if the local player can execute server-side C# scripts. + /// bool CanScript(); + + /// + /// Check if the local player can open the admin menu. + /// bool CanAdminMenu(); void Initialize(); diff --git a/Content.Client/Chat/ChatBox.cs b/Content.Client/Chat/ChatBox.cs index 627956a7d4..75e65ff87f 100644 --- a/Content.Client/Chat/ChatBox.cs +++ b/Content.Client/Chat/ChatBox.cs @@ -106,15 +106,12 @@ namespace Content.Client.Chat AllButton.OnToggled += OnFilterToggled; LocalButton.OnToggled += OnFilterToggled; OOCButton.OnToggled += OnFilterToggled; + AdminButton.OnToggled += OnFilterToggled; hBox.AddChild(AllButton); hBox.AddChild(LocalButton); hBox.AddChild(OOCButton); - if(AdminButton != null) - { - AdminButton.OnToggled += OnFilterToggled; - hBox.AddChild(AdminButton); - } + hBox.AddChild(AdminButton); AddChild(outerVBox); } diff --git a/Content.Client/UserInterface/Permissions/PermissionsEui.cs b/Content.Client/UserInterface/Permissions/PermissionsEui.cs index 4801472f3e..6751962dc7 100644 --- a/Content.Client/UserInterface/Permissions/PermissionsEui.cs +++ b/Content.Client/UserInterface/Permissions/PermissionsEui.cs @@ -241,7 +241,7 @@ namespace Content.Client.UserInterface.Permissions al.AddChild(rankControl); - var flagsText = AdminFlagsExt.PosNegFlagsText(admin.PosFlags, admin.NegFlags); + var flagsText = AdminFlagsHelper.PosNegFlagsText(admin.PosFlags, admin.NegFlags); al.AddChild(new Label { @@ -264,7 +264,7 @@ namespace Content.Client.UserInterface.Permissions foreach (var kv in s.AdminRanks) { var rank = kv.Value; - var flagsText = string.Join(' ', AdminFlagsExt.FlagsToNames(rank.Flags).Select(f => $"+{f}")); + var flagsText = string.Join(' ', AdminFlagsHelper.FlagsToNames(rank.Flags).Select(f => $"+{f}")); _menu.AdminRanksList.AddChild(new Label {Text = rank.Name}); _menu.AdminRanksList.AddChild(new Label { @@ -390,7 +390,7 @@ namespace Content.Client.UserInterface.Permissions VSeparationOverride = 0 }; - foreach (var flag in AdminFlagsExt.AllFlags) + foreach (var flag in AdminFlagsHelper.AllFlags) { // Can only grant out perms you also have yourself. // Primarily intended to prevent people giving themselves +HOST with +PERMISSIONS but generalized. @@ -527,7 +527,7 @@ namespace Content.Client.UserInterface.Permissions NameEdit = new LineEdit { - PlaceHolder = "Rank name", + PlaceHolder = Loc.GetString("Rank name"), }; if (data != null) @@ -538,7 +538,7 @@ namespace Content.Client.UserInterface.Permissions SaveButton = new Button {Text = Loc.GetString("Save"), SizeFlagsHorizontal = SizeFlags.ShrinkEnd | SizeFlags.Expand}; var flagsBox = new VBoxContainer(); - foreach (var flag in AdminFlagsExt.AllFlags) + foreach (var flag in AdminFlagsHelper.AllFlags) { // Can only grant out perms you also have yourself. // Primarily intended to prevent people giving themselves +HOST with +PERMISSIONS but generalized. diff --git a/Content.Server/Administration/AdminManager.cs b/Content.Server/Administration/AdminManager.cs index 8048346941..4c1b9fc4d5 100644 --- a/Content.Server/Administration/AdminManager.cs +++ b/Content.Server/Administration/AdminManager.cs @@ -95,7 +95,7 @@ namespace Content.Server.Administration _chat.DispatchServerMessage(session, Loc.GetString("You are now an admin.")); var plyData = session.ContentData()!; - plyData.ExplicitlyDeadminned = true; + plyData.ExplicitlyDeadminned = false; reg.Data.Active = true; _chat.SendAdminAnnouncement(Loc.GetString("{0} re-adminned themselves.", session.Name)); @@ -203,7 +203,7 @@ namespace Content.Server.Administration if (map.TryGetNode("Flags", out var flagsNode)) { var flagNames = flagsNode.AsString().Split(",", StringSplitOptions.RemoveEmptyEntries); - var flags = AdminFlagsExt.NamesToFlags(flagNames); + var flags = AdminFlagsHelper.NamesToFlags(flagNames); foreach (var cmd in commands) { if (!_adminCommands.TryGetValue(cmd, out var exFlags)) @@ -316,7 +316,7 @@ namespace Content.Server.Administration var data = new AdminData { Title = Loc.GetString("Host"), - Flags = AdminFlagsExt.Everything, + Flags = AdminFlagsHelper.Everything, }; return (data, null, true); @@ -335,12 +335,12 @@ namespace Content.Server.Administration if (dbData.AdminRank != null) { - flags = AdminFlagsExt.NamesToFlags(dbData.AdminRank.Flags.Select(p => p.Flag)); + flags = AdminFlagsHelper.NamesToFlags(dbData.AdminRank.Flags.Select(p => p.Flag)); } foreach (var dbFlag in dbData.Flags) { - var flag = AdminFlagsExt.NameToFlag(dbFlag.Flag); + var flag = AdminFlagsHelper.NameToFlag(dbFlag.Flag); if (dbFlag.Negative) { flags &= ~flag; diff --git a/Content.Server/Administration/Commands/DeAdminCommand.cs b/Content.Server/Administration/Commands/DeAdminCommand.cs index 44135a0c1b..81a61194cf 100644 --- a/Content.Server/Administration/Commands/DeAdminCommand.cs +++ b/Content.Server/Administration/Commands/DeAdminCommand.cs @@ -6,7 +6,7 @@ using Robust.Shared.IoC; #nullable enable -namespace Content.Server.Administration +namespace Content.Server.Administration.Commands { [UsedImplicitly] [AdminCommand(AdminFlags.None)] diff --git a/Content.Server/Administration/Commands/ReAdminCommand.cs b/Content.Server/Administration/Commands/ReAdminCommand.cs index edd197b71f..8f78442689 100644 --- a/Content.Server/Administration/Commands/ReAdminCommand.cs +++ b/Content.Server/Administration/Commands/ReAdminCommand.cs @@ -4,7 +4,7 @@ using Robust.Shared.IoC; #nullable enable -namespace Content.Server.Administration +namespace Content.Server.Administration.Commands { [AnyCommand] public class ReAdminCommand : IClientCommand diff --git a/Content.Server/Administration/PermissionsEui.cs b/Content.Server/Administration/PermissionsEui.cs index 018b3a83ab..2c5ba53b08 100644 --- a/Content.Server/Administration/PermissionsEui.cs +++ b/Content.Server/Administration/PermissionsEui.cs @@ -39,17 +39,17 @@ namespace Content.Server.Administration StateDirty(); LoadFromDb(); - _adminManager.OnPermsChanged += AdminManagerOnOnPermsChanged; + _adminManager.OnPermsChanged += AdminManagerOnPermsChanged; } public override void Closed() { base.Closed(); - _adminManager.OnPermsChanged -= AdminManagerOnOnPermsChanged; + _adminManager.OnPermsChanged -= AdminManagerOnPermsChanged; } - private void AdminManagerOnOnPermsChanged(AdminPermsChangedEventArgs obj) + private void AdminManagerOnPermsChanged(AdminPermsChangedEventArgs obj) { // Close UI if user loses +PERMISSIONS. if (obj.Player == Player && !UserAdminFlagCheck(AdminFlags.Permissions)) @@ -72,8 +72,8 @@ namespace Content.Server.Administration { Admins = _admins.Select(p => new PermissionsEuiState.AdminData { - PosFlags = AdminFlagsExt.NamesToFlags(p.a.Flags.Where(f => !f.Negative).Select(f => f.Flag)), - NegFlags = AdminFlagsExt.NamesToFlags(p.a.Flags.Where(f => f.Negative).Select(f => f.Flag)), + PosFlags = AdminFlagsHelper.NamesToFlags(p.a.Flags.Where(f => !f.Negative).Select(f => f.Flag)), + NegFlags = AdminFlagsHelper.NamesToFlags(p.a.Flags.Where(f => f.Negative).Select(f => f.Flag)), Title = p.a.Title, RankId = p.a.AdminRankId, UserId = new NetUserId(p.a.UserId), @@ -82,7 +82,7 @@ namespace Content.Server.Administration AdminRanks = _adminRanks.ToDictionary(a => a.Id, a => new PermissionsEuiState.AdminRankData { - Flags = AdminFlagsExt.NamesToFlags(a.Flags.Select(p => p.Flag)), + Flags = AdminFlagsHelper.NamesToFlags(a.Flags.Select(p => p.Flag)), Name = a.Name }) }; @@ -185,7 +185,7 @@ namespace Content.Server.Administration await _db.UpdateAdminRankAsync(rank); - var flagText = string.Join(' ', AdminFlagsExt.FlagsToNames(ur.Flags).Select(f => $"+{f}")); + var flagText = string.Join(' ', AdminFlagsHelper.FlagsToNames(ur.Flags).Select(f => $"+{f}")); Logger.InfoS("admin.perms", $"{Player} updated admin rank {rank.Name}/{flagText}."); _adminManager.ReloadAdminsWithRank(ur.Id); @@ -207,7 +207,7 @@ namespace Content.Server.Administration await _db.AddAdminRankAsync(rank); - var flagText = string.Join(' ', AdminFlagsExt.FlagsToNames(ar.Flags).Select(f => $"+{f}")); + var flagText = string.Join(' ', AdminFlagsHelper.FlagsToNames(ar.Flags).Select(f => $"+{f}")); Logger.InfoS("admin.perms", $"{Player} added admin rank {rank.Name}/{flagText}."); } @@ -272,7 +272,7 @@ namespace Content.Server.Administration var name = playerRecord?.LastSeenUserName ?? ua.UserId.ToString(); var title = ua.Title ?? ""; - var flags = AdminFlagsExt.PosNegFlagsText(ua.PosFlags, ua.NegFlags); + var flags = AdminFlagsHelper.PosNegFlagsText(ua.PosFlags, ua.NegFlags); Logger.InfoS("admin.perms", $"{Player} updated admin {name} to {title}/{rankName}/{flags}"); @@ -347,7 +347,7 @@ namespace Content.Server.Administration await _db.AddAdminAsync(admin); var title = ca.Title ?? ""; - var flags = AdminFlagsExt.PosNegFlagsText(ca.PosFlags, ca.NegFlags); + var flags = AdminFlagsHelper.PosNegFlagsText(ca.PosFlags, ca.NegFlags); Logger.InfoS("admin.perms", $"{Player} added admin {name} as {title}/{rankName}/{flags}"); @@ -392,7 +392,7 @@ namespace Content.Server.Administration ret = rank.Name; - var rankFlags = AdminFlagsExt.NamesToFlags(rank.Flags.Select(p => p.Flag)); + var rankFlags = AdminFlagsHelper.NamesToFlags(rank.Flags.Select(p => p.Flag)); if (!UserAdminFlagCheck(rankFlags)) { // Can't assign a rank with flags you don't have yourself. @@ -421,8 +421,8 @@ namespace Content.Server.Administration private static List GenAdminFlagList(AdminFlags posFlags, AdminFlags negFlags) { - var posFlagList = AdminFlagsExt.FlagsToNames(posFlags); - var negFlagList = AdminFlagsExt.FlagsToNames(negFlags); + var posFlagList = AdminFlagsHelper.FlagsToNames(posFlags); + var negFlagList = AdminFlagsHelper.FlagsToNames(negFlags); return posFlagList .Select(f => new AdminFlag {Negative = false, Flag = f}) @@ -432,7 +432,7 @@ namespace Content.Server.Administration private static List GenRankFlagList(AdminFlags flags) { - return AdminFlagsExt.FlagsToNames(flags).Select(f => new AdminRankFlag {Flag = f}).ToList(); + return AdminFlagsHelper.FlagsToNames(flags).Select(f => new AdminRankFlag {Flag = f}).ToList(); } private bool UserAdminFlagCheck(AdminFlags flags) @@ -442,8 +442,8 @@ namespace Content.Server.Administration private bool CanTouchAdmin(Admin admin) { - var posFlags = AdminFlagsExt.NamesToFlags(admin.Flags.Where(f => !f.Negative).Select(f => f.Flag)); - var rankFlags = AdminFlagsExt.NamesToFlags( + var posFlags = AdminFlagsHelper.NamesToFlags(admin.Flags.Where(f => !f.Negative).Select(f => f.Flag)); + var rankFlags = AdminFlagsHelper.NamesToFlags( admin.AdminRank?.Flags.Select(f => f.Flag) ?? Array.Empty()); var totalFlags = posFlags | rankFlags; @@ -452,7 +452,7 @@ namespace Content.Server.Administration private bool CanTouchRank(DbAdminRank rank) { - var rankFlags = AdminFlagsExt.NamesToFlags(rank.Flags.Select(f => f.Flag)); + var rankFlags = AdminFlagsHelper.NamesToFlags(rank.Flags.Select(f => f.Flag)); return UserAdminFlagCheck(rankFlags); } diff --git a/Content.Server/Chat/ChatManager.cs b/Content.Server/Chat/ChatManager.cs index 1d35847a94..afeb0c11b2 100644 --- a/Content.Server/Chat/ChatManager.cs +++ b/Content.Server/Chat/ChatManager.cs @@ -214,7 +214,6 @@ namespace Content.Server.Chat var clients = _playerManager .GetPlayersBy(x => x.AttachedEntity != null && x.AttachedEntity.HasComponent()) .Select(p => p.ConnectedClient); - ; var msg = _netManager.CreateNetMessage(); msg.Channel = ChatChannel.Dead; diff --git a/Content.Shared/Administration/AdminData.cs b/Content.Shared/Administration/AdminData.cs index 70ff7c6876..8c56f901ed 100644 --- a/Content.Shared/Administration/AdminData.cs +++ b/Content.Shared/Administration/AdminData.cs @@ -2,35 +2,64 @@ namespace Content.Shared.Administration { + /// + /// Represents data for a single server admin. + /// public sealed class AdminData { - public const string DefaultTitle = "Admin"; - // Can be false if they're de-adminned with the ability to re-admin. + /// + /// Whether the admin is currently active. This can be false if they have de-adminned mid-round. + /// public bool Active; + + /// + /// The admin's title. + /// public string? Title; + + /// + /// The admin's permission flags. + /// public AdminFlags Flags; + /// + /// Checks whether this admin has an admin flag. + /// + /// The flags to check. Multiple flags can be specified, they must all be held. + /// False if this admin is not or does not have all the flags specified. public bool HasFlag(AdminFlags flag) { return Active && (Flags & flag) == flag; } + /// + /// Check if this admin can open the VV menu. + /// public bool CanViewVar() { return HasFlag(AdminFlags.VarEdit); } + /// + /// Check if this admin can spawn stuff in with the entity/tile spawn panel. + /// public bool CanAdminPlace() { return HasFlag(AdminFlags.Spawn); } + /// + /// Check if this admin can execute server-side C# scripts. + /// public bool CanScript() { return HasFlag(AdminFlags.Host); } + /// + /// Check if this admin can open the admin menu. + /// public bool CanAdminMenu() { return HasFlag(AdminFlags.Admin); diff --git a/Content.Shared/Administration/AdminFlagsExt.cs b/Content.Shared/Administration/AdminFlagsHelper.cs similarity index 63% rename from Content.Shared/Administration/AdminFlagsExt.cs rename to Content.Shared/Administration/AdminFlagsHelper.cs index 0c018e8c3a..9d5bba3baa 100644 --- a/Content.Shared/Administration/AdminFlagsExt.cs +++ b/Content.Shared/Administration/AdminFlagsHelper.cs @@ -5,16 +5,28 @@ using System.Numerics; namespace Content.Shared.Administration { - public static class AdminFlagsExt + /// + /// Contains various helper methods for working with admin flags. + /// + public static class AdminFlagsHelper { + // As you can tell from the boatload of bitwise ops, + // writing this class was genuinely fun. + private static readonly Dictionary NameFlagsMap = new Dictionary(); private static readonly string[] FlagsNameMap = new string[32]; + /// + /// Every admin flag in the game, at once! + /// public static readonly AdminFlags Everything; + /// + /// A list of all individual admin flags. + /// public static readonly IReadOnlyList AllFlags; - static AdminFlagsExt() + static AdminFlagsHelper() { var t = typeof(AdminFlags); var flags = (AdminFlags[]) Enum.GetValues(t); @@ -24,6 +36,8 @@ namespace Content.Shared.Administration { var name = value.ToString().ToUpper(); + // If, in the future, somebody adds a combined admin flag or something for convenience, + // ignore it. if (BitOperations.PopCount((uint) value) != 1) { continue; @@ -38,6 +52,15 @@ namespace Content.Shared.Administration AllFlags = allFlags.ToArray(); } + /// + /// Converts an enumerable of admin flag names to a bitfield. + /// + /// + /// The flags must all be uppercase. + /// + /// + /// Thrown if a string that is not a valid admin flag is contained in . + /// public static AdminFlags NamesToFlags(IEnumerable names) { var flags = AdminFlags.None; @@ -54,11 +77,23 @@ namespace Content.Shared.Administration return flags; } + /// + /// Gets the flag bit for an admin flag name. + /// + /// + /// The flag name must be all uppercase. + /// + /// + /// Thrown if is not a valid admin flag name. + /// public static AdminFlags NameToFlag(string name) { return NameFlagsMap[name]; } + /// + /// Converts a bitfield of admin flags to an array of all the flag names set. + /// public static string[] FlagsToNames(AdminFlags flags) { var array = new string[BitOperations.PopCount((uint) flags)]; diff --git a/Content.Tests/Shared/Administration/AdminFlagsExtTest.cs b/Content.Tests/Shared/Administration/AdminFlagsExtTest.cs index e44a8a1719..e2bc95a8f7 100644 --- a/Content.Tests/Shared/Administration/AdminFlagsExtTest.cs +++ b/Content.Tests/Shared/Administration/AdminFlagsExtTest.cs @@ -17,7 +17,7 @@ namespace Content.Tests.Shared.Administration { var names = namesConcat.Split(",", StringSplitOptions.RemoveEmptyEntries); - Assert.That(AdminFlagsExt.NamesToFlags(names), Is.EqualTo(flags)); + Assert.That(AdminFlagsHelper.NamesToFlags(names), Is.EqualTo(flags)); } [Test] @@ -29,7 +29,7 @@ namespace Content.Tests.Shared.Administration { var names = namesConcat.Split(",", StringSplitOptions.RemoveEmptyEntries); - Assert.That(AdminFlagsExt.FlagsToNames(flags), Is.EquivalentTo(names)); + Assert.That(AdminFlagsHelper.FlagsToNames(flags), Is.EquivalentTo(names)); } } } From f8f177f43af7861fac9ff283565c4947cb70a451 Mon Sep 17 00:00:00 2001 From: Pieter-Jan Briers Date: Tue, 10 Nov 2020 22:00:07 +0100 Subject: [PATCH 11/11] More comments and a fix. --- Content.Client/Eui/BaseEui.cs | 19 +++++++- Content.Server/Eui/BaseEui.cs | 80 +++++++++++++++++++++++--------- Content.Server/Eui/EuiManager.cs | 2 +- 3 files changed, 75 insertions(+), 26 deletions(-) diff --git a/Content.Client/Eui/BaseEui.cs b/Content.Client/Eui/BaseEui.cs index 65ebe3d213..ee5ad3c974 100644 --- a/Content.Client/Eui/BaseEui.cs +++ b/Content.Client/Eui/BaseEui.cs @@ -19,34 +19,49 @@ namespace Content.Client.Eui IoCManager.InjectDependencies(this); } - public void Initialize(EuiManager mgr, uint id) + internal void Initialize(EuiManager mgr, uint id) { Manager = mgr; Id = id; } + /// + /// Called when the EUI is opened by the server. + /// public virtual void Opened() { } + /// + /// Called when the EUI is closed by the server. + /// public virtual void Closed() { } + /// + /// Called when a new state comes in from the server. + /// public virtual void HandleState(EuiStateBase state) { } + /// + /// Called when a message comes in from the server. + /// public virtual void HandleMessage(EuiMessageBase msg) { } + /// + /// Send a message to the server-side implementation. + /// protected void SendMessage(EuiMessageBase msg) { var netMsg = _netManager.CreateNetMessage(); netMsg.Id = Id; netMsg.Message = msg; - + _netManager.ClientSendMessage(netMsg); } } diff --git a/Content.Server/Eui/BaseEui.cs b/Content.Server/Eui/BaseEui.cs index 5b58cc4a4a..cd2dc4c52f 100644 --- a/Content.Server/Eui/BaseEui.cs +++ b/Content.Server/Eui/BaseEui.cs @@ -9,46 +9,56 @@ using Robust.Shared.IoC; namespace Content.Server.Eui { + /// + /// Base class to implement server-side for an EUI. + /// + /// + /// An EUI is a system for making a relatively-easy connection between client and server + /// for the purposes of UIs. + /// + /// + /// An equivalently named class much exist server side for an EUI to work. + /// It will be instantiated, opened and closed automatically. + /// public abstract class BaseEui { private bool _isStateDirty = false; + /// + /// The player that this EUI is open for. + /// + public IPlayerSession Player { get; private set; } = default!; public bool IsShutDown { get; private set; } public EuiManager Manager { get; private set; } = default!; - public IPlayerSession Player { get; private set; } = default!; public uint Id { get; private set; } - public void Initialize(EuiManager manager, IPlayerSession player, uint id) - { - Manager = manager; - Player = player; - Id = id; - Opened(); - } - + /// + /// Called when the UI has been opened. Do initializing logic here. + /// public virtual void Opened() { } + /// + /// Called when the UI has been closed. + /// public virtual void Closed() { } + /// + /// Called when a message comes in from the client. + /// public virtual void HandleMessage(EuiMessageBase msg) { } - public void Shutdown() - { - Closed(); - IsShutDown = true; - } - /// /// Mark the current UI state as dirty and queue for an update. /// + /// public void StateDirty() { if (_isStateDirty) @@ -60,17 +70,43 @@ namespace Content.Server.Eui Manager.QueueStateUpdate(this); } + /// + /// Called some time after has been called + /// to get a new UI state that can be sent to the client. + /// public virtual EuiStateBase GetNewState() { throw new NotSupportedException(); } + /// + /// Send a message to the client-side EUI. + /// + public void SendMessage(EuiMessageBase message) + { + var netMgr = IoCManager.Resolve(); + var msg = netMgr.CreateNetMessage(); + msg.Id = Id; + msg.Message = message; + + netMgr.ServerSendMessage(msg, Player.ConnectedClient); + } + + /// + /// Close the EUI, breaking the connection between client and server. + /// public void Close() { Manager.CloseEui(this); } - public void DoStateUpdate() + internal void Shutdown() + { + Closed(); + IsShutDown = true; + } + + internal void DoStateUpdate() { _isStateDirty = false; @@ -84,14 +120,12 @@ namespace Content.Server.Eui netMgr.ServerSendMessage(msg, Player.ConnectedClient); } - public void SendMessage(EuiMessageBase message) + internal void Initialize(EuiManager manager, IPlayerSession player, uint id) { - var netMgr = IoCManager.Resolve(); - var msg = netMgr.CreateNetMessage(); - msg.Id = Id; - msg.Message = message; - - netMgr.ServerSendMessage(msg, Player.ConnectedClient); + Manager = manager; + Player = player; + Id = id; + Opened(); } } } diff --git a/Content.Server/Eui/EuiManager.cs b/Content.Server/Eui/EuiManager.cs index 80e0246d08..b5a8ae2a87 100644 --- a/Content.Server/Eui/EuiManager.cs +++ b/Content.Server/Eui/EuiManager.cs @@ -82,7 +82,7 @@ namespace Content.Server.Eui public void CloseEui(BaseEui eui) { - eui.Closed(); + eui.Shutdown(); _playerData[eui.Player].OpenUIs.Remove(eui.Id); var msg = _net.CreateNetMessage();