diff --git a/src/Tgstation.Server.Api/Models/EntityId.cs b/src/Tgstation.Server.Api/Models/EntityId.cs index bf0319ae6e..a33d3f294c 100644 --- a/src/Tgstation.Server.Api/Models/EntityId.cs +++ b/src/Tgstation.Server.Api/Models/EntityId.cs @@ -8,6 +8,7 @@ namespace Tgstation.Server.Api.Models /// /// The ID of the entity. /// + [RequestOptions(FieldPresence.Required)] [RequestOptions(FieldPresence.Ignored, PutOnly = true)] public virtual long? Id { get; set; } } diff --git a/src/Tgstation.Server.Api/Models/ErrorCode.cs b/src/Tgstation.Server.Api/Models/ErrorCode.cs index 830fe9135c..2e219e12de 100644 --- a/src/Tgstation.Server.Api/Models/ErrorCode.cs +++ b/src/Tgstation.Server.Api/Models/ErrorCode.cs @@ -178,7 +178,7 @@ namespace Tgstation.Server.Api.Models ConfigurationDirectoryNotEmpty, /// - /// Tried to clone a repository with a missing property. + /// Tried to clone a repository with a missing property. /// [Obsolete("Deprecated")] [Description("Cannot clone repository with missing origin field!")] diff --git a/src/Tgstation.Server.Api/Models/Internal/CompileJob.cs b/src/Tgstation.Server.Api/Models/Internal/CompileJob.cs index 1776727b18..425d63eec2 100644 --- a/src/Tgstation.Server.Api/Models/Internal/CompileJob.cs +++ b/src/Tgstation.Server.Api/Models/Internal/CompileJob.cs @@ -7,7 +7,7 @@ namespace Tgstation.Server.Api.Models.Internal /// /// Represents a deployment run. /// - public class CompileJob : EntityId + public abstract class CompileJob : EntityId { /// /// The .dme file used for compilation @@ -19,7 +19,6 @@ namespace Tgstation.Server.Api.Models.Internal /// Textual output of DM /// [Required] - [ResponseOptions] public string? Output { get; set; } /// @@ -29,8 +28,9 @@ namespace Tgstation.Server.Api.Models.Internal public Guid? DirectoryName { get; set; } /// - /// The minimum required to run the 's output + /// The minimum required to run the 's output. /// + [ResponseOptions] public DreamDaemonSecurity? MinimumSecurityLevel { get; set; } /// diff --git a/src/Tgstation.Server.Api/Models/Internal/InstancePermissionSet.cs b/src/Tgstation.Server.Api/Models/Internal/InstancePermissionSet.cs index d41063b01a..ff2c24fa4a 100644 --- a/src/Tgstation.Server.Api/Models/Internal/InstancePermissionSet.cs +++ b/src/Tgstation.Server.Api/Models/Internal/InstancePermissionSet.cs @@ -11,6 +11,7 @@ namespace Tgstation.Server.Api.Models.Internal /// /// The of the the belongs to /// + [RequestOptions(FieldPresence.Required)] public long PermissionSetId { get; set; } /// diff --git a/src/Tgstation.Server.Api/Models/Internal/RepositoryApiBase.cs b/src/Tgstation.Server.Api/Models/Internal/RepositoryApiBase.cs index de8fe2997a..2a52068888 100644 --- a/src/Tgstation.Server.Api/Models/Internal/RepositoryApiBase.cs +++ b/src/Tgstation.Server.Api/Models/Internal/RepositoryApiBase.cs @@ -11,6 +11,7 @@ namespace Tgstation.Server.Api.Models.Internal /// The branch or tag HEAD points to. /// [StringLength(Limits.MaximumStringLength)] + [ResponseOptions] public string? Reference { get; set; } } } diff --git a/src/Tgstation.Server.Api/Models/Internal/UserApiBase.cs b/src/Tgstation.Server.Api/Models/Internal/UserApiBase.cs index c2455905cd..5f988fd475 100644 --- a/src/Tgstation.Server.Api/Models/Internal/UserApiBase.cs +++ b/src/Tgstation.Server.Api/Models/Internal/UserApiBase.cs @@ -13,6 +13,7 @@ namespace Tgstation.Server.Api.Models.Internal /// /// The directly associated with the . /// + [ResponseOptions] public PermissionSet? PermissionSet { get; set; } /// diff --git a/src/Tgstation.Server.Api/Models/Internal/UserModelBase.cs b/src/Tgstation.Server.Api/Models/Internal/UserModelBase.cs index 053913fd39..f30e5f84d3 100644 --- a/src/Tgstation.Server.Api/Models/Internal/UserModelBase.cs +++ b/src/Tgstation.Server.Api/Models/Internal/UserModelBase.cs @@ -6,13 +6,12 @@ namespace Tgstation.Server.Api.Models.Internal /// /// Represents a server user. /// - public abstract class UserModelBase : NamedEntity + public abstract class UserModelBase : UserName { /// /// If the is enabled since users cannot be deleted. System users cannot be disabled /// [Required] - [ResponseOptions] public bool? Enabled { get; set; } /// @@ -20,7 +19,6 @@ namespace Tgstation.Server.Api.Models.Internal /// [Required] [RequestOptions(FieldPresence.Ignored)] - [ResponseOptions] public DateTimeOffset? CreatedAt { get; set; } /// diff --git a/src/Tgstation.Server.Api/Models/NamedEntity.cs b/src/Tgstation.Server.Api/Models/NamedEntity.cs index 59d0954ba6..42dc14d95c 100644 --- a/src/Tgstation.Server.Api/Models/NamedEntity.cs +++ b/src/Tgstation.Server.Api/Models/NamedEntity.cs @@ -5,15 +5,14 @@ namespace Tgstation.Server.Api.Models /// /// Base class for named entities. /// - public class NamedEntity : EntityId + public abstract class NamedEntity : EntityId { /// /// The name of the entity represented by the . /// [Required] - [ResponseOptions] [RequestOptions(FieldPresence.Required, PutOnly = true)] [StringLength(Limits.MaximumIndexableStringLength, MinimumLength = 1)] - public string? Name { get; set; } + public virtual string? Name { get; set; } } } diff --git a/src/Tgstation.Server.Api/Models/Request/ChatBotCreateRequest.cs b/src/Tgstation.Server.Api/Models/Request/ChatBotCreateRequest.cs index 632698bc1f..c40979b9b2 100644 --- a/src/Tgstation.Server.Api/Models/Request/ChatBotCreateRequest.cs +++ b/src/Tgstation.Server.Api/Models/Request/ChatBotCreateRequest.cs @@ -1,9 +1,11 @@ +using Tgstation.Server.Api.Models.Internal; + namespace Tgstation.Server.Api.Models { /// /// Represents a request to update a chat bot. /// - public sealed class ChatBotCreateRequest : ChatBotUpdateRequest + public sealed class ChatBotCreateRequest : ChatBotApiBase { } } diff --git a/src/Tgstation.Server.Api/Models/Request/InstanceCreateRequest.cs b/src/Tgstation.Server.Api/Models/Request/InstanceCreateRequest.cs index 08cea8e22a..a04a9e5e61 100644 --- a/src/Tgstation.Server.Api/Models/Request/InstanceCreateRequest.cs +++ b/src/Tgstation.Server.Api/Models/Request/InstanceCreateRequest.cs @@ -3,7 +3,7 @@ namespace Tgstation.Server.Api.Models /// /// A request to create an . /// - public sealed class InstanceCreateRequest : InstanceUpdateRequest + public sealed class InstanceCreateRequest : Instance { } } diff --git a/src/Tgstation.Server.Api/Models/Request/RepositoryCloneRequest.cs b/src/Tgstation.Server.Api/Models/Request/RepositoryCreateRequest.cs similarity index 87% rename from src/Tgstation.Server.Api/Models/Request/RepositoryCloneRequest.cs rename to src/Tgstation.Server.Api/Models/Request/RepositoryCreateRequest.cs index 919a41e237..acae78657d 100644 --- a/src/Tgstation.Server.Api/Models/Request/RepositoryCloneRequest.cs +++ b/src/Tgstation.Server.Api/Models/Request/RepositoryCreateRequest.cs @@ -6,7 +6,7 @@ namespace Tgstation.Server.Api.Models /// /// Represents a request to clone the repository. /// - public sealed class RepositoryCloneRequest : RepositoryApiBase + public sealed class RepositoryCreateRequest : RepositoryApiBase { /// /// The origin URL to clone. diff --git a/src/Tgstation.Server.Api/Models/RequestOptionsAttribute.cs b/src/Tgstation.Server.Api/Models/RequestOptionsAttribute.cs index 3bdd990192..26184bd666 100644 --- a/src/Tgstation.Server.Api/Models/RequestOptionsAttribute.cs +++ b/src/Tgstation.Server.Api/Models/RequestOptionsAttribute.cs @@ -5,7 +5,7 @@ namespace Tgstation.Server.Api.Models /// /// Indicates the for fields in models. /// - [AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = false)] + [AttributeUsage(AttributeTargets.Property, AllowMultiple = true, Inherited = false)] public sealed class RequestOptionsAttribute : Attribute { /// diff --git a/src/Tgstation.Server.Api/Models/Response/ConfigurationFileResponse.cs b/src/Tgstation.Server.Api/Models/Response/ConfigurationFileResponse.cs index 071dbd9d81..bbc9a24b38 100644 --- a/src/Tgstation.Server.Api/Models/Response/ConfigurationFileResponse.cs +++ b/src/Tgstation.Server.Api/Models/Response/ConfigurationFileResponse.cs @@ -9,6 +9,7 @@ namespace Tgstation.Server.Api.Models public string? Path { get; set; } /// + [ResponseOptions] public string? LastReadHash { get; set; } /// diff --git a/src/Tgstation.Server.Api/Models/Response/DreamDaemonResponse.cs b/src/Tgstation.Server.Api/Models/Response/DreamDaemonResponse.cs index 37548a09d7..acf6865dbc 100644 --- a/src/Tgstation.Server.Api/Models/Response/DreamDaemonResponse.cs +++ b/src/Tgstation.Server.Api/Models/Response/DreamDaemonResponse.cs @@ -24,6 +24,7 @@ namespace Tgstation.Server.Api.Models /// The current . /// [EnumDataType(typeof(WatchdogStatus))] + [ResponseOptions] public WatchdogStatus? Status { get; set; } /// diff --git a/src/Tgstation.Server.Api/Models/Response/JobResponse.cs b/src/Tgstation.Server.Api/Models/Response/JobResponse.cs index dc67d3c80c..13de9d5fef 100644 --- a/src/Tgstation.Server.Api/Models/Response/JobResponse.cs +++ b/src/Tgstation.Server.Api/Models/Response/JobResponse.cs @@ -8,13 +8,13 @@ namespace Tgstation.Server.Api.Models /// /// The that started the job /// - public UserResponse? StartedBy { get; set; } + public UserName? StartedBy { get; set; } /// /// The that cancelled the job /// [ResponseOptions] - public UserResponse? CancelledBy { get; set; } + public UserName? CancelledBy { get; set; } /// /// Optional progress between 0 and 100 inclusive diff --git a/src/Tgstation.Server.Api/Models/Response/UserGroupResponse.cs b/src/Tgstation.Server.Api/Models/Response/UserGroupResponse.cs index 151ed68f95..9a897f8a00 100644 --- a/src/Tgstation.Server.Api/Models/Response/UserGroupResponse.cs +++ b/src/Tgstation.Server.Api/Models/Response/UserGroupResponse.cs @@ -9,6 +9,6 @@ namespace Tgstation.Server.Api.Models /// /// The s the has. /// - public ICollection? Users { get; set; } + public ICollection? Users { get; set; } } } diff --git a/src/Tgstation.Server.Api/Models/Response/UserResponse.cs b/src/Tgstation.Server.Api/Models/Response/UserResponse.cs index 895b498c45..01feceff92 100644 --- a/src/Tgstation.Server.Api/Models/Response/UserResponse.cs +++ b/src/Tgstation.Server.Api/Models/Response/UserResponse.cs @@ -10,6 +10,6 @@ namespace Tgstation.Server.Api.Models /// The who created this /// [Required] - public NamedEntity? CreatedBy { get; set; } + public UserName? CreatedBy { get; set; } } } diff --git a/src/Tgstation.Server.Api/Models/ResponseOptionsAttribute.cs b/src/Tgstation.Server.Api/Models/ResponseOptionsAttribute.cs index f7e86ac9af..a8c42e0f4c 100644 --- a/src/Tgstation.Server.Api/Models/ResponseOptionsAttribute.cs +++ b/src/Tgstation.Server.Api/Models/ResponseOptionsAttribute.cs @@ -3,7 +3,7 @@ using System; namespace Tgstation.Server.Api.Models { /// - /// Indicates API fields that may be null on response. + /// Indicates the response of API fields. Changes it from to by default. /// [AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)] public sealed class ResponseOptionsAttribute : Attribute diff --git a/src/Tgstation.Server.Api/Models/TestMerge.cs b/src/Tgstation.Server.Api/Models/TestMerge.cs index 7d8623f1b4..5ab802f96f 100644 --- a/src/Tgstation.Server.Api/Models/TestMerge.cs +++ b/src/Tgstation.Server.Api/Models/TestMerge.cs @@ -8,6 +8,6 @@ namespace Tgstation.Server.Api.Models /// /// The of the user who created the . /// - public NamedEntity? MergedBy { get; set; } + public UserName? MergedBy { get; set; } } } diff --git a/src/Tgstation.Server.Api/Models/UserName.cs b/src/Tgstation.Server.Api/Models/UserName.cs new file mode 100644 index 0000000000..8044489a2d --- /dev/null +++ b/src/Tgstation.Server.Api/Models/UserName.cs @@ -0,0 +1,33 @@ +namespace Tgstation.Server.Api.Models +{ + /// + /// Base class for user names. + /// + public class UserName : NamedEntity + { + /// + [RequestOptions(FieldPresence.Optional)] + public override string? Name + { + get => base.Name; + set => base.Name = value; + } + + /// + /// Create a copy of the as a given . + /// + /// The child of to create. + /// A new copied from . + protected virtual TResultType CreateUserName() where TResultType : UserName, new() => new TResultType + { + Id = Id, + Name = Name + }; + + /// + /// Create a copy of the . + /// + /// A new copied from . + public UserName CreateUserName() => CreateUserName(); + } +} diff --git a/src/Tgstation.Server.Client/Components/IRepositoryClient.cs b/src/Tgstation.Server.Client/Components/IRepositoryClient.cs index 3a8d2274d1..7d1b25911b 100644 --- a/src/Tgstation.Server.Client/Components/IRepositoryClient.cs +++ b/src/Tgstation.Server.Client/Components/IRepositoryClient.cs @@ -27,10 +27,10 @@ namespace Tgstation.Server.Client.Components /// /// Clones a /// - /// The . + /// The . /// The for the operation /// A resulting in the / - Task Clone(RepositoryCloneRequest repository, CancellationToken cancellationToken); + Task Clone(RepositoryCreateRequest repository, CancellationToken cancellationToken); /// /// Deletes the repository. diff --git a/src/Tgstation.Server.Client/Components/RepositoryClient.cs b/src/Tgstation.Server.Client/Components/RepositoryClient.cs index c62312298c..25c807d2c9 100644 --- a/src/Tgstation.Server.Client/Components/RepositoryClient.cs +++ b/src/Tgstation.Server.Client/Components/RepositoryClient.cs @@ -31,7 +31,7 @@ namespace Tgstation.Server.Client.Components } /// - public Task Clone(RepositoryCloneRequest repository, CancellationToken cancellationToken) => apiClient.Create(Routes.Repository, repository, instance.Id!.Value, cancellationToken); + public Task Clone(RepositoryCreateRequest repository, CancellationToken cancellationToken) => apiClient.Create(Routes.Repository, repository, instance.Id!.Value, cancellationToken); /// public Task Delete(CancellationToken cancellationToken) => apiClient.Delete(Routes.Repository, instance.Id!.Value, cancellationToken); diff --git a/src/Tgstation.Server.Host/Components/Chat/Providers/DiscordProvider.cs b/src/Tgstation.Server.Host/Components/Chat/Providers/DiscordProvider.cs index 4b60ad50fb..a1dcb0b409 100644 --- a/src/Tgstation.Server.Host/Components/Chat/Providers/DiscordProvider.cs +++ b/src/Tgstation.Server.Host/Components/Chat/Providers/DiscordProvider.cs @@ -80,7 +80,7 @@ namespace Tgstation.Server.Host.Components.Chat.Providers IJobManager jobManager, IAssemblyInformationProvider assemblyInformationProvider, ILogger logger, - Models.ChatBot chatBot) + ChatBot chatBot) : base(jobManager, logger, chatBot) { this.assemblyInformationProvider = assemblyInformationProvider ?? throw new ArgumentNullException(nameof(assemblyInformationProvider)); diff --git a/src/Tgstation.Server.Host/Controllers/BridgeController.cs b/src/Tgstation.Server.Host/Controllers/BridgeController.cs index 8bb613ff4d..d9397874b4 100644 --- a/src/Tgstation.Server.Host/Controllers/BridgeController.cs +++ b/src/Tgstation.Server.Host/Controllers/BridgeController.cs @@ -19,6 +19,7 @@ namespace Tgstation.Server.Host.Controllers [Route("/Bridge")] [Produces(MediaTypeNames.Application.Json)] [ApiController] + [ApiExplorerSettings(IgnoreApi = true)] public class BridgeController : Controller { /// diff --git a/src/Tgstation.Server.Host/Controllers/ChatController.cs b/src/Tgstation.Server.Host/Controllers/ChatController.cs index 0c981f2395..18501e9b36 100644 --- a/src/Tgstation.Server.Host/Controllers/ChatController.cs +++ b/src/Tgstation.Server.Host/Controllers/ChatController.cs @@ -11,6 +11,7 @@ using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using Tgstation.Server.Api; using Tgstation.Server.Api.Models; +using Tgstation.Server.Api.Models.Internal; using Tgstation.Server.Api.Rights; using Tgstation.Server.Host.Components; using Tgstation.Server.Host.Database; @@ -65,14 +66,14 @@ namespace Tgstation.Server.Host.Controllers /// /// Create a new chat bot . /// - /// The . + /// The . /// The for the operation. /// A resulting in the for the operation. /// Created successfully. [HttpPut] [TgsAuthorize(ChatBotRights.Create)] [ProducesResponseType(typeof(ChatBotResponse), 201)] - public async Task Create([FromBody] ChatBotUpdateRequest model, CancellationToken cancellationToken) + public async Task Create([FromBody] ChatBotCreateRequest model, CancellationToken cancellationToken) { if (model == null) throw new ArgumentNullException(nameof(model)); @@ -278,7 +279,7 @@ namespace Tgstation.Server.Host.Controllers bool anySettingsModified = false; - bool CheckModified(Expression> expression, ChatBotRights requiredRight) + bool CheckModified(Expression> expression, ChatBotRights requiredRight) { var memberSelectorExpression = (MemberExpression)expression.Body; var property = (PropertyInfo)memberSelectorExpression.Member; @@ -350,10 +351,10 @@ namespace Tgstation.Server.Host.Controllers /// /// Perform some basic validation of a given . /// - /// The to validate. + /// The to validate. /// If the is being created. /// An to respond with or . - private IActionResult StandardModelChecks(ChatBotUpdateRequest model, bool forCreation) + private IActionResult StandardModelChecks(ChatBotApiBase model, bool forCreation) { if (model.ReconnectionInterval == 0) throw new InvalidOperationException("RecconnectionInterval cannot be zero!"); diff --git a/src/Tgstation.Server.Host/Controllers/ControlPanelController.cs b/src/Tgstation.Server.Host/Controllers/ControlPanelController.cs index 71de870e2c..b632f7e690 100644 --- a/src/Tgstation.Server.Host/Controllers/ControlPanelController.cs +++ b/src/Tgstation.Server.Host/Controllers/ControlPanelController.cs @@ -17,6 +17,7 @@ namespace Tgstation.Server.Host.Controllers /// Controller for the web control panel. /// [Route(Application.ControlPanelRoute)] + [ApiExplorerSettings(IgnoreApi = true)] public class ControlPanelController : Controller { /// diff --git a/src/Tgstation.Server.Host/Controllers/DreamDaemonController.cs b/src/Tgstation.Server.Host/Controllers/DreamDaemonController.cs index a2e30f82dc..08c95e2e07 100644 --- a/src/Tgstation.Server.Host/Controllers/DreamDaemonController.cs +++ b/src/Tgstation.Server.Host/Controllers/DreamDaemonController.cs @@ -67,17 +67,17 @@ namespace Tgstation.Server.Host.Controllers /// /// The for the operation. /// A resulting in the of the operation. - /// to launch the watchdog started successfully. + /// to launch the watchdog started successfully. [HttpPut] [TgsAuthorize(DreamDaemonRights.Start)] - [ProducesResponseType(typeof(Api.Models.JobResponse), 202)] + [ProducesResponseType(typeof(JobResponse), 202)] public Task Create(CancellationToken cancellationToken) => WithComponentInstance(async instance => { if (instance.Watchdog.Status != WatchdogStatus.Offline) return Conflict(new ErrorMessageResponse(ErrorCode.WatchdogRunning)); - var job = new Models.Job + var job = new Job { Description = "Launch DreamDaemon", CancelRight = (ulong)DreamDaemonRights.Shutdown, @@ -298,18 +298,18 @@ namespace Tgstation.Server.Host.Controllers #pragma warning restore CA1502 /// - /// Creates a to restart the Watchdog. It will not start if it wasn't already running. + /// Creates a to restart the Watchdog. It will not start if it wasn't already running. /// /// The for the operation /// A resulting in the of the request - /// Restart started successfully. + /// Restart started successfully. [HttpPatch] [TgsAuthorize(DreamDaemonRights.Restart)] - [ProducesResponseType(typeof(Api.Models.JobResponse), 202)] + [ProducesResponseType(typeof(JobResponse), 202)] public Task Restart(CancellationToken cancellationToken) => WithComponentInstance(async instance => { - var job = new Models.Job + var job = new Job { Instance = Instance, CancelRightsType = RightsType.DreamDaemon, @@ -332,18 +332,18 @@ namespace Tgstation.Server.Host.Controllers }); /// - /// Creates a to generate a DreamDaemon process dump. + /// Creates a to generate a DreamDaemon process dump. /// /// The for the operation /// A resulting in the of the request - /// Dump started successfully. + /// Dump started successfully. [HttpPatch(Routes.Diagnostics)] [TgsAuthorize(DreamDaemonRights.CreateDump)] - [ProducesResponseType(typeof(Api.Models.JobResponse), 202)] + [ProducesResponseType(typeof(JobResponse), 202)] public Task CreateDump(CancellationToken cancellationToken) => WithComponentInstance(async instance => { - var job = new Models.Job + var job = new Job { Instance = Instance, CancelRightsType = RightsType.DreamDaemon, diff --git a/src/Tgstation.Server.Host/Controllers/InstanceController.cs b/src/Tgstation.Server.Host/Controllers/InstanceController.cs index c420f5dd9c..05721a4957 100644 --- a/src/Tgstation.Server.Host/Controllers/InstanceController.cs +++ b/src/Tgstation.Server.Host/Controllers/InstanceController.cs @@ -116,7 +116,7 @@ namespace Tgstation.Server.Host.Controllers swarmConfiguration = swarmConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(swarmConfigurationOptions)); } - async Task CreateDefaultInstance(InstanceUpdateRequest initialSettings, CancellationToken cancellationToken) + async Task CreateDefaultInstance(InstanceCreateRequest initialSettings, CancellationToken cancellationToken) { var ddPort = await portAllocator.GetAvailablePort(1, false, cancellationToken).ConfigureAwait(false); if (!ddPort.HasValue) @@ -222,7 +222,7 @@ namespace Tgstation.Server.Host.Controllers /// /// Create or attach an . /// - /// The settings. + /// The . /// The for the operation. /// A resulting in the of the request. /// Instance attached successfully. @@ -231,7 +231,7 @@ namespace Tgstation.Server.Host.Controllers [TgsAuthorize(InstanceManagerRights.Create)] [ProducesResponseType(typeof(InstanceResponse), 200)] [ProducesResponseType(typeof(InstanceResponse), 201)] - public async Task Create([FromBody] InstanceUpdateRequest model, CancellationToken cancellationToken) + public async Task Create([FromBody] InstanceCreateRequest model, CancellationToken cancellationToken) { if (model == null) throw new ArgumentNullException(nameof(model)); diff --git a/src/Tgstation.Server.Host/Controllers/RepositoryController.cs b/src/Tgstation.Server.Host/Controllers/RepositoryController.cs index 5360ab1d2b..9108bd3aa8 100644 --- a/src/Tgstation.Server.Host/Controllers/RepositoryController.cs +++ b/src/Tgstation.Server.Host/Controllers/RepositoryController.cs @@ -130,7 +130,7 @@ namespace Tgstation.Server.Host.Controllers /// /// Begin cloning the repository if it doesn't exist. /// - /// The . + /// The . /// The for the operation. /// A resulting in the of the request. /// The repository was created successfully and the to clone it has begun. @@ -139,7 +139,7 @@ namespace Tgstation.Server.Host.Controllers [TgsAuthorize(RepositoryRights.SetOrigin)] [ProducesResponseType(typeof(RepositoryResponse), 201)] [ProducesResponseType(typeof(ErrorMessageResponse), 410)] - public async Task Create([FromBody] RepositoryCloneRequest model, CancellationToken cancellationToken) + public async Task Create([FromBody] RepositoryCreateRequest model, CancellationToken cancellationToken) { if (model == null) throw new ArgumentNullException(nameof(model)); diff --git a/src/Tgstation.Server.Host/Controllers/SwarmController.cs b/src/Tgstation.Server.Host/Controllers/SwarmController.cs index 473df47a38..18659b6972 100644 --- a/src/Tgstation.Server.Host/Controllers/SwarmController.cs +++ b/src/Tgstation.Server.Host/Controllers/SwarmController.cs @@ -21,6 +21,7 @@ namespace Tgstation.Server.Host.Controllers [Route(SwarmConstants.ControllerRoute)] [Produces(MediaTypeNames.Application.Json)] [ApiController] + [ApiExplorerSettings(IgnoreApi = true)] public sealed class SwarmController : Controller { /// diff --git a/src/Tgstation.Server.Host/Controllers/UserController.cs b/src/Tgstation.Server.Host/Controllers/UserController.cs index 1dba52295f..50e16681ee 100644 --- a/src/Tgstation.Server.Host/Controllers/UserController.cs +++ b/src/Tgstation.Server.Host/Controllers/UserController.cs @@ -66,7 +66,7 @@ namespace Tgstation.Server.Host.Controllers } /// - /// Check if a given has a valid specified. + /// Check if a given has a valid specified. /// /// The to check. /// If this is a new . @@ -105,7 +105,7 @@ namespace Tgstation.Server.Host.Controllers /// /// Create a new . /// - /// The to create. + /// The . /// The for the operation. /// A resulting in the of the operation. /// created successfully. @@ -114,7 +114,7 @@ namespace Tgstation.Server.Host.Controllers [TgsAuthorize(AdministrationRights.WriteUsers)] [ProducesResponseType(typeof(UserResponse), 201)] #pragma warning disable CA1502, CA1506 - public async Task Create([FromBody] UserUpdateRequest model, CancellationToken cancellationToken) + public async Task Create([FromBody] UserCreateRequest model, CancellationToken cancellationToken) { if (model == null) throw new ArgumentNullException(nameof(model)); @@ -180,7 +180,7 @@ namespace Tgstation.Server.Host.Controllers Logger.LogInformation("Created new user {0} ({1})", dbUser.Name, dbUser.Id); - return Created(dbUser.ToApi(true)); + return Created(dbUser.ToApi()); } #pragma warning restore CA1502, CA1506 @@ -191,11 +191,13 @@ namespace Tgstation.Server.Host.Controllers /// The for the operation. /// A resulting in the of the operation. /// updated successfully. + /// updated successfully. Not returned due to lack of permissions. /// Requested does not exist. /// Requested does not exist. [HttpPost] [TgsAuthorize(AdministrationRights.WriteUsers | AdministrationRights.EditOwnPassword | AdministrationRights.EditOwnOAuthConnections)] [ProducesResponseType(typeof(UserResponse), 200)] + [ProducesResponseType(204)] [ProducesResponseType(typeof(ErrorMessageResponse), 404)] [ProducesResponseType(typeof(ErrorMessageResponse), 410)] #pragma warning disable CA1502 // TODO: Decomplexify @@ -334,14 +336,11 @@ namespace Tgstation.Server.Host.Controllers Logger.LogInformation("Updated user {0} ({1})", originalUser.Name, originalUser.Id); // return id only if not a self update and cannot read users - return Json( - AuthenticationContext.User.Id == originalUser.Id - || callerAdministrationRights.HasFlag(AdministrationRights.ReadUsers) - ? originalUser.ToApi(true) - : new UserResponse - { - Id = originalUser.Id - }); + var canReadBack = AuthenticationContext.User.Id == originalUser.Id + || callerAdministrationRights.HasFlag(AdministrationRights.ReadUsers); + return canReadBack + ? Json(originalUser.ToApi()) + : NoContent(); } #pragma warning restore CA1506 #pragma warning restore CA1502 @@ -354,7 +353,7 @@ namespace Tgstation.Server.Host.Controllers [HttpGet] [TgsAuthorize] [ProducesResponseType(typeof(UserResponse), 200)] - public IActionResult Read() => Json(AuthenticationContext.User.ToApi(true)); + public IActionResult Read() => Json(AuthenticationContext.User.ToApi()); /// /// List all s in the server. @@ -421,7 +420,7 @@ namespace Tgstation.Server.Host.Controllers if (user.CanonicalName == Models.User.CanonicalizeName(Models.User.TgsSystemUserName)) return Forbid(); - return Json(user.ToApi(true)); + return Json(user.ToApi()); } /// diff --git a/src/Tgstation.Server.Host/Core/SwaggerConfiguration.cs b/src/Tgstation.Server.Host/Core/SwaggerConfiguration.cs index e1a68eab5e..2f2c9518cc 100644 --- a/src/Tgstation.Server.Host/Core/SwaggerConfiguration.cs +++ b/src/Tgstation.Server.Host/Core/SwaggerConfiguration.cs @@ -9,6 +9,7 @@ using System.Collections.Generic; using System.Linq; using System.Net; using System.Net.Mime; +using System.Reflection; using Tgstation.Server.Api; using Tgstation.Server.Api.Models; using Tgstation.Server.Host.Controllers; @@ -120,12 +121,15 @@ namespace Tgstation.Server.Host.Core }); } + /// + /// Generates the OpenAPI schema ID for a given . + /// + /// The to generate a schema ID for. + /// The generated schema ID for . static string GenerateSchemaId(Type type) { - if (type == typeof(NamedEntity)) + if (type == typeof(UserName)) return "ShallowUserResponse"; - if (type == typeof(Api.Models.Internal.UserGroup)) - return "ShallowUserGroupResponse"; if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(PaginatedResponse<>)) return $"Paginated{type.GenericTypeArguments.First().Name}"; @@ -378,24 +382,12 @@ namespace Tgstation.Server.Host.Core Schema = productHeaderSchema }); - var pathsToRemove = new List(); - var filteredControllers = new string[] - { - nameof(BridgeController), - nameof(ControlPanelController), - nameof(SwarmController), - }; - + var allSchemas = context + .SchemaRepository + .Schemas; foreach (var path in swaggerDoc.Paths) foreach (var operation in path.Value.Operations.Select(x => x.Value)) { - if (filteredControllers.Any( - x => operation.OperationId.StartsWith(x, StringComparison.Ordinal))) - { - pathsToRemove.Add(path.Key); - continue; - } - operation.Parameters.Insert(0, new OpenApiParameter { Reference = new OpenApiReference @@ -415,12 +407,120 @@ namespace Tgstation.Server.Host.Core }); } - foreach (var filteredPath in pathsToRemove) - swaggerDoc.Paths.Remove(filteredPath); - AddDefaultResponses(swaggerDoc); } + /// + /// Applies the , , and to of a given . + /// + /// The root . + /// The current . + static void ApplyAttributesForRootSchema(OpenApiSchema rootSchema, SchemaFilterContext context) + { + // tune up the descendants + rootSchema.Nullable = false; + var rootSchemaId = GenerateSchemaId(context.Type); + var rootRequestSchema = rootSchemaId.EndsWith("Request", StringComparison.Ordinal); + var rootResponseSchema = rootSchemaId.EndsWith("Response", StringComparison.Ordinal); + var isPutRequest = rootRequestSchema && rootSchemaId.EndsWith("CreateRequest", StringComparison.Ordinal); + + Tuple> GetTypeFromKvp(Type currentType, KeyValuePair kvp, IDictionary schemaDictionary) + { + var propertyInfo = currentType + .GetProperties() + .Single(x => x.Name.Equals(kvp.Key, StringComparison.OrdinalIgnoreCase)); + + return Tuple.Create( + propertyInfo, + kvp.Key, + kvp.Value, + schemaDictionary); + } + + var subSchemaStack = new Stack>>( + rootSchema.Properties.Select(x => GetTypeFromKvp(context.Type, x, rootSchema.Properties))); + + while (subSchemaStack.Count > 0) + { + var tuple = subSchemaStack.Pop(); + var subSchema = tuple.Item3; + + if (subSchema.Reference != null) + // can't mangle references per request + continue; + + var subSchemaPropertyInfo = tuple.Item1; + + if (subSchema.Properties != null + && !subSchemaPropertyInfo + .PropertyType + .GetInterfaces() + .Any(x => x == typeof(IEnumerable))) + foreach (var kvp in subSchema.Properties) + subSchemaStack.Push(GetTypeFromKvp(subSchemaPropertyInfo.PropertyType, kvp, subSchema.Properties)); + + var attributes = subSchemaPropertyInfo + .GetCustomAttributes(); + var responsePresence = attributes + .OfType() + .FirstOrDefault() + ?.Presence + ?? FieldPresence.Required; + var requestOptions = attributes + .OfType() + .OrderBy(x => x.PutOnly) // Process PUTs last + .ToList(); + + if (requestOptions.Any() && requestOptions.All(x => x.Presence == FieldPresence.Ignored && !x.PutOnly)) + subSchema.ReadOnly = true; + + var subSchemaId = tuple.Item2; + var subSchemaOwningDictionary = tuple.Item4; + if (rootResponseSchema) + { + subSchema.Nullable = responsePresence == FieldPresence.Optional; + if (responsePresence == FieldPresence.Ignored) + subSchemaOwningDictionary.Remove(subSchemaId); + } + else if (rootRequestSchema) + { + subSchema.Nullable = true; + var lastOptionWasIgnored = false; + foreach (var requestOption in requestOptions) + { + var validForThisRequest = !requestOption.PutOnly || isPutRequest; + if (!validForThisRequest) + continue; + + lastOptionWasIgnored = false; + switch (requestOption.Presence) + { + case FieldPresence.Ignored: + lastOptionWasIgnored = true; + break; + case FieldPresence.Optional: + subSchema.Nullable = true; + break; + case FieldPresence.Required: + subSchema.Nullable = false; + break; + default: + throw new InvalidOperationException($"Invalid FieldPresence: {requestOption.Presence}!"); + } + } + + if (lastOptionWasIgnored) + subSchemaOwningDictionary.Remove(subSchemaId); + } + else if (responsePresence == FieldPresence.Required + && requestOptions.All(x => x.Presence == FieldPresence.Required && !x.PutOnly)) + subSchema.Nullable = false; + + // otherwise, we have to assume it's a shared schema + // use what Swagger thinks the nullability is by default + } + } + /// public void Apply(OpenApiSchema schema, SchemaFilterContext context) { @@ -432,53 +532,18 @@ namespace Tgstation.Server.Host.Core // Nothing is required schema.Required.Clear(); - // Could be nullable type, make sure to get the right one - Type nonNullableType = context.Type.IsConstructedGenericType - ? context.Type.GenericTypeArguments.First() - : context.Type; - - if (context.MemberInfo != null) - { - var schemaId = GenerateSchemaId(context.MemberInfo.DeclaringType); - var responseOptions = context.MemberInfo.GetCustomAttributes(typeof(ResponseOptionsAttribute), true).OfType().FirstOrDefault(); - var requestOptions = context.MemberInfo.GetCustomAttributes(typeof(RequestOptionsAttribute), true).OfType().FirstOrDefault(); - - if (schemaId.EndsWith("Response", StringComparison.Ordinal)) - if (responseOptions?.Presence == FieldPresence.Optional - || (nonNullableType != context.Type - && !context.Type.GetInterfaces().Any(x => x == typeof(IEnumerable)))) - schema.Nullable = true; - else if (responseOptions?.Presence == FieldPresence.Ignored) - schema.WriteOnly = true; - else - schema.Nullable = false; - else if (requestOptions != null) - if (schemaId.EndsWith("Request", StringComparison.Ordinal)) - { - var isPutRequest = schemaId.Contains("Create", StringComparison.OrdinalIgnoreCase); - if (!(requestOptions.PutOnly && !isPutRequest)) - if (requestOptions.Presence == FieldPresence.Required) - context.SchemaRepository.Schemas.Remove(schemaId); - else if (requestOptions.Presence == FieldPresence.Ignored) - schema.ReadOnly = true; - } - else if (responseOptions?.Presence != FieldPresence.Optional && requestOptions.Presence == FieldPresence.Required) - // if something has request required and no optional response, it's always required - schema.Nullable = false; - else - schema.Nullable = true; - else - { - if (responseOptions?.Presence == FieldPresence.Ignored) - context.SchemaRepository.Schemas.Remove(schemaId); - schema.Nullable = true; - } - } + if (context.MemberInfo == null) + ApplyAttributesForRootSchema(schema, context); if (!schema.Enum?.Any() ?? false) return; - OpenApiEnumVarNamesExtension.Apply(schema, nonNullableType); + // Could be nullable type, make sure to get the right one + Type firstGenericArgumentOrType = context.Type.IsConstructedGenericType + ? context.Type.GenericTypeArguments.First() + : context.Type; + + OpenApiEnumVarNamesExtension.Apply(schema, firstGenericArgumentOrType); } /// diff --git a/src/Tgstation.Server.Host/Models/Job.cs b/src/Tgstation.Server.Host/Models/Job.cs index e5b22b2e86..ec58dcc866 100644 --- a/src/Tgstation.Server.Host/Models/Job.cs +++ b/src/Tgstation.Server.Host/Models/Job.cs @@ -31,13 +31,13 @@ namespace Tgstation.Server.Host.Models StartedAt = StartedAt, StoppedAt = StoppedAt, Cancelled = Cancelled, - CancelledBy = CancelledBy?.ToApi(false), + CancelledBy = CancelledBy?.CreateUserName(), CancelRight = CancelRight, CancelRightsType = CancelRightsType, Description = Description, ExceptionDetails = ExceptionDetails, ErrorCode = ErrorCode, - StartedBy = StartedBy.ToApi(false) + StartedBy = StartedBy.CreateUserName() }; } } diff --git a/src/Tgstation.Server.Host/Models/TestMerge.cs b/src/Tgstation.Server.Host/Models/TestMerge.cs index e1286c6900..0ee060f536 100644 --- a/src/Tgstation.Server.Host/Models/TestMerge.cs +++ b/src/Tgstation.Server.Host/Models/TestMerge.cs @@ -37,7 +37,7 @@ namespace Tgstation.Server.Host.Models TitleAtMerge = TitleAtMerge, Comment = Comment, Id = Id, - MergedBy = MergedBy.ToApi(false), + MergedBy = MergedBy.CreateUserName(), Number = Number, TargetCommitSha = TargetCommitSha, Url = Url diff --git a/src/Tgstation.Server.Host/Models/User.cs b/src/Tgstation.Server.Host/Models/User.cs index 1af92e0efb..620ec5d748 100644 --- a/src/Tgstation.Server.Host/Models/User.cs +++ b/src/Tgstation.Server.Host/Models/User.cs @@ -40,7 +40,7 @@ namespace Tgstation.Server.Host.Models public PermissionSet PermissionSet { get; set; } /// - /// The uppercase invariant of + /// The uppercase invariant of /// [Required] [StringLength(Limits.MaximumIndexableStringLength, MinimumLength = 1)] @@ -67,43 +67,35 @@ namespace Tgstation.Server.Host.Models public ICollection OAuthConnections { get; set; } /// - /// Change a into a . + /// Change a into a . /// - /// The . + /// The . /// The . public static string CanonicalizeName(string name) => name?.ToUpperInvariant() ?? throw new ArgumentNullException(nameof(name)); /// - /// See + /// Generate a from . /// - /// If we should recurse on - /// If rights and system identifier should be shown + /// If we should recurse on . /// A new . - UserResponse ToApi(bool recursive, bool showDetails) => new UserResponse + UserResponse CreateUserResponse(bool recursive) { - CreatedAt = showDetails ? CreatedAt : null, - CreatedBy = showDetails && recursive ? CreatedBy?.ToApi(false, false) : null, - Enabled = showDetails ? Enabled : null, - Id = Id, - Name = Name, - SystemIdentifier = showDetails ? SystemIdentifier : null, - OAuthConnections = showDetails - ? OAuthConnections - ?.Select(x => x.ToApi()) - .ToList() - : null, - Group = showDetails ? Group?.ToApi(false) : null, - PermissionSet = showDetails ? PermissionSet?.ToApi() : null, - }; + var result = CreateUserName(); + if (recursive) + result.CreatedBy = CreatedBy?.CreateUserName(); - /// - /// Convert the to it's API form - /// - /// If system identifier, oauth connections, and group/permission set should be shown. - /// A new - public UserResponse ToApi(bool showDetails) => ToApi(true, showDetails); + result.CreatedAt = CreatedAt; + result.Enabled = Enabled; + result.SystemIdentifier = SystemIdentifier; + result.OAuthConnections = OAuthConnections + ?.Select(x => x.ToApi()) + .ToList(); + result.Group = Group?.ToApi(false); + result.PermissionSet = PermissionSet?.ToApi(); + return result; + } /// - public UserResponse ToApi() => ToApi(true); + public UserResponse ToApi() => CreateUserResponse(true); } } diff --git a/src/Tgstation.Server.Host/Models/UserGroup.cs b/src/Tgstation.Server.Host/Models/UserGroup.cs index 2bda19ea99..07d89939c3 100644 --- a/src/Tgstation.Server.Host/Models/UserGroup.cs +++ b/src/Tgstation.Server.Host/Models/UserGroup.cs @@ -31,10 +31,9 @@ namespace Tgstation.Server.Host.Models PermissionSet = PermissionSet.ToApi(), Users = showUsers ? Users - ?.Select(x => x.ToApi(false)) - .OfType() + ?.Select(x => x.CreateUserName()) .ToList() - ?? new List() + ?? new List() : null, }; diff --git a/tests/Tgstation.Server.Tests/Instance/RepositoryTest.cs b/tests/Tgstation.Server.Tests/Instance/RepositoryTest.cs index a03b221cc5..9acf03d40e 100644 --- a/tests/Tgstation.Server.Tests/Instance/RepositoryTest.cs +++ b/tests/Tgstation.Server.Tests/Instance/RepositoryTest.cs @@ -45,7 +45,7 @@ namespace Tgstation.Server.Tests.Instance Assert.IsNull(initalRepo.ActiveJob); const string Origin = "https://github.com/tgstation/tgstation-server"; - var cloneRequest = new RepositoryCloneRequest + var cloneRequest = new RepositoryCreateRequest { Origin = new Uri(Origin), Reference = workingBranch, diff --git a/tests/Tgstation.Server.Tests/InstanceManagerTest.cs b/tests/Tgstation.Server.Tests/InstanceManagerTest.cs index ef4e3355d2..a1ec5d7040 100644 --- a/tests/Tgstation.Server.Tests/InstanceManagerTest.cs +++ b/tests/Tgstation.Server.Tests/InstanceManagerTest.cs @@ -34,7 +34,7 @@ namespace Tgstation.Server.Tests ChatBotLimit = 2 }, cancellationToken); - static InstanceCreateRequest FromResponse(InstanceResponse response) => new InstanceCreateRequest + static TRequestType FromResponse(InstanceResponse response) where TRequestType : Api.Models.Instance, new() => new TRequestType { Id = response.Id, Path = response.Path, @@ -75,7 +75,7 @@ namespace Tgstation.Server.Tests Name = "NonEmptyTest" }, cancellationToken).ConfigureAwait(false); - await Assert.ThrowsExceptionAsync(() => instanceManagerClient.CreateOrAttach(FromResponse(firstTest), cancellationToken)).ConfigureAwait(false); + await Assert.ThrowsExceptionAsync(() => instanceManagerClient.CreateOrAttach(FromResponse(firstTest), cancellationToken)).ConfigureAwait(false); Assert.IsTrue(Directory.Exists(firstTest.Path)); //can't create instances in installation directory @@ -135,7 +135,7 @@ namespace Tgstation.Server.Tests //online it for real for component tests firstTest.Online = true; firstTest.ConfigurationType = ConfigurationType.HostWrite; - firstTest = await instanceManagerClient.Update(FromResponse(firstTest), cancellationToken).ConfigureAwait(false); + firstTest = await instanceManagerClient.Update(FromResponse(firstTest), cancellationToken).ConfigureAwait(false); Assert.AreEqual(true, firstTest.Online); Assert.AreEqual(ConfigurationType.HostWrite, firstTest.ConfigurationType); Assert.IsTrue(Directory.Exists(firstTest.Path)); @@ -175,7 +175,7 @@ namespace Tgstation.Server.Tests await ApiAssert.ThrowsException(() => instanceManagerClient.Detach(firstTest, cancellationToken), ErrorCode.InstanceDetachOnline).ConfigureAwait(false); firstTest.Online = false; - firstTest = await instanceManagerClient.Update(FromResponse(firstTest), cancellationToken).ConfigureAwait(false); + firstTest = await instanceManagerClient.Update(FromResponse(firstTest), cancellationToken).ConfigureAwait(false); await instanceManagerClient.Detach(firstTest, cancellationToken).ConfigureAwait(false); @@ -183,7 +183,7 @@ namespace Tgstation.Server.Tests Assert.IsTrue(File.Exists(attachPath)); //can recreate detached instance - firstTest = await instanceManagerClient.CreateOrAttach(FromResponse(firstTest), cancellationToken).ConfigureAwait(false); + firstTest = await instanceManagerClient.CreateOrAttach(FromResponse(firstTest), cancellationToken).ConfigureAwait(false); // Test updating only with SetChatBotLimit works var current = await usersClient.Read(cancellationToken); @@ -210,7 +210,7 @@ namespace Tgstation.Server.Tests //but only if the attach file exists await instanceManagerClient.Detach(firstTest, cancellationToken).ConfigureAwait(false); File.Delete(attachPath); - await ApiAssert.ThrowsException(() => instanceManagerClient.CreateOrAttach(FromResponse(firstTest), cancellationToken), ErrorCode.InstanceAtExistingPath).ConfigureAwait(false); + await ApiAssert.ThrowsException(() => instanceManagerClient.CreateOrAttach(FromResponse(firstTest), cancellationToken), ErrorCode.InstanceAtExistingPath).ConfigureAwait(false); } } }