diff --git a/build/BuildDox.ps1 b/build/BuildDox.ps1
index e41968ebb8..a88025e3d7 100644
--- a/build/BuildDox.ps1
+++ b/build/BuildDox.ps1
@@ -25,6 +25,9 @@ if($publish_dox){
git config user.email "tgstation-server@tgstation13.org"
echo '# THIS BRANCH IS AUTO GENERATED BY APPVEYOR CI' > README.md
+ # Add in the swagger specification
+ mv C:/swagger.json "$doxdir/swagger.json"
+
# Need to create a .nojekyll file to allow filenames starting with an underscore
# to be seen on the gh-pages site. Therefore creating an empty .nojekyll file.
echo "" > .nojekyll
diff --git a/docs/API.dox b/docs/API.dox
index 15416efd79..532597365d 100644
--- a/docs/API.dox
+++ b/docs/API.dox
@@ -3,6 +3,14 @@
@tableofcontents
+@section api_swag OpenAPI Spec
+
+TGS4 has a, from code, generated OpenAPI 3.0 specification. It is much more authorative than these documents.
+
+The most up to date version should be found here: https://raw.githubusercontent.com/tgstation/tgstation-server/gh-pages/swagger.json
+
+You can use the API explorer SwaggerUI to interact with it: https://petstore.swagger.io
+
@section api_intro Introduction
The TGS4 API is designed to be a fully realized RESTful service. Once hosted, follow the specified protocol for developing new clients or one off requests that provide full control over the server
diff --git a/src/Tgstation.Server.Api/ApiHeaders.cs b/src/Tgstation.Server.Api/ApiHeaders.cs
index b1be91ff6f..ff31d48e6d 100644
--- a/src/Tgstation.Server.Api/ApiHeaders.cs
+++ b/src/Tgstation.Server.Api/ApiHeaders.cs
@@ -7,6 +7,7 @@ using System.Globalization;
using System.Linq;
using System.Net.Http.Headers;
using System.Reflection;
+using System.Text;
namespace Tgstation.Server.Api
{
@@ -23,27 +24,32 @@ namespace Tgstation.Server.Api
///
/// The header key
///
- const string ApiVersionHeader = "Api";
-
- ///
- /// The header key
- ///
- const string UsernameHeader = "Username";
+ public const string ApiVersionHeader = "api";
///
/// The header key
///
- const string InstanceIdHeader = "Instance";
+ public const string InstanceIdHeader = "instance";
///
/// The JWT authentication header scheme
///
- const string JwtAuthenticationScheme = "Bearer";
+ public const string JwtAuthenticationScheme = "bearer";
///
- /// The password authentication header scheme
+ /// The JWT authentication header scheme
///
- const string PasswordAuthenticationScheme = "Password";
+ public const string BasicAuthenticationScheme = "basic";
+
+ ///
+ /// The header key
+ ///
+ const string UsernameHeader = "username";
+
+ ///
+ /// The basic authentication header scheme
+ ///
+ const string PasswordAuthenticationScheme = "password";
///
/// The current
@@ -180,7 +186,9 @@ namespace Tgstation.Server.Api
InstanceId = instanceId;
}
- switch (scheme)
+#pragma warning disable CA1308 // Normalize strings to uppercase
+ switch (scheme.ToLowerInvariant())
+#pragma warning restore CA1308 // Normalize strings to uppercase
{
case JwtAuthenticationScheme:
Token = parameter;
@@ -197,6 +205,25 @@ namespace Tgstation.Server.Api
if (fail)
throw new InvalidOperationException("Missing Username header!");
break;
+ case BasicAuthenticationScheme:
+ string joinedString;
+ try
+ {
+ var base64Bytes = Convert.FromBase64String(parameter);
+ joinedString = Encoding.UTF8.GetString(base64Bytes);
+ }
+ catch
+ {
+ throw new InvalidOperationException("Invalid basic Authorization header!");
+ }
+
+ var basicAuthSplits = joinedString.Split(new char[] { ':' }, StringSplitOptions.RemoveEmptyEntries);
+ if (basicAuthSplits.Length < 2)
+ throw new InvalidOperationException("Invalid basic Authorization header!");
+
+ Username = basicAuthSplits.First();
+ Password = String.Concat(basicAuthSplits.Skip(1));
+ break;
default:
throw new InvalidOperationException("Invalid authentication scheme!");
}
@@ -241,10 +268,9 @@ namespace Tgstation.Server.Api
if (IsTokenAuthentication)
headers.Authorization = new AuthenticationHeaderValue(JwtAuthenticationScheme, Token);
else
- {
- headers.Authorization = new AuthenticationHeaderValue(PasswordAuthenticationScheme, Password);
- headers.Add(UsernameHeader, Username);
- }
+ headers.Authorization = new AuthenticationHeaderValue(
+ BasicAuthenticationScheme,
+ Convert.ToBase64String(Encoding.UTF8.GetBytes($"{Username}:{Password}")));
headers.UserAgent.Add(new ProductInfoHeaderValue(UserAgent));
headers.Add(ApiVersionHeader, new ProductHeaderValue(AssemblyName.Name, ApiVersion.ToString()).ToString());
diff --git a/src/Tgstation.Server.Api/Models/EntityId.cs b/src/Tgstation.Server.Api/Models/EntityId.cs
new file mode 100644
index 0000000000..879b3b2ec8
--- /dev/null
+++ b/src/Tgstation.Server.Api/Models/EntityId.cs
@@ -0,0 +1,13 @@
+namespace Tgstation.Server.Api.Models
+{
+ ///
+ /// Base of s.
+ ///
+ public class EntityId
+ {
+ ///
+ /// The ID of the entity.
+ ///
+ public long Id { get; set; }
+ }
+}
diff --git a/src/Tgstation.Server.Api/Models/Internal/CompileJob.cs b/src/Tgstation.Server.Api/Models/Internal/CompileJob.cs
index c659c49737..204657b883 100644
--- a/src/Tgstation.Server.Api/Models/Internal/CompileJob.cs
+++ b/src/Tgstation.Server.Api/Models/Internal/CompileJob.cs
@@ -6,13 +6,8 @@ namespace Tgstation.Server.Api.Models.Internal
///
/// Represents a run of
///
- public class CompileJob
+ public class CompileJob : EntityId
{
- ///
- /// The ID of the job
- ///
- public long Id { get; set; }
-
///
/// The .dme file used for compilation
///
diff --git a/src/Tgstation.Server.Api/Models/Internal/Job.cs b/src/Tgstation.Server.Api/Models/Internal/Job.cs
index e64bb68115..945d0d6aaa 100644
--- a/src/Tgstation.Server.Api/Models/Internal/Job.cs
+++ b/src/Tgstation.Server.Api/Models/Internal/Job.cs
@@ -7,13 +7,8 @@ namespace Tgstation.Server.Api.Models.Internal
///
/// Represents a long running job
///
- public class Job
+ public class Job : EntityId
{
- ///
- /// The ID
- ///
- public long Id { get; set; }
-
///
/// English description of the
///
diff --git a/src/Tgstation.Server.Api/Rights/RightsHelper.cs b/src/Tgstation.Server.Api/Rights/RightsHelper.cs
index 6df923331e..52947aaa6d 100644
--- a/src/Tgstation.Server.Api/Rights/RightsHelper.cs
+++ b/src/Tgstation.Server.Api/Rights/RightsHelper.cs
@@ -60,7 +60,7 @@ namespace Tgstation.Server.Api.Rights
/// A representing the claim role name
public static string RoleName(RightsType rightsType, Enum right)
{
- var enumType = TypeMap[rightsType];
+ var enumType = RightToType(rightsType);
return String.Concat(enumType.Name, '.', Enum.GetName(enumType, right));
}
diff --git a/src/Tgstation.Server.Api/Routes.cs b/src/Tgstation.Server.Api/Routes.cs
index 2ba4b4b19b..6a2da6c8f0 100644
--- a/src/Tgstation.Server.Api/Routes.cs
+++ b/src/Tgstation.Server.Api/Routes.cs
@@ -78,6 +78,11 @@ namespace Tgstation.Server.Api
///
public const string Jobs = Root + nameof(Models.Job);
+ ///
+ /// The postfix for list operations
+ ///
+ public const string List = "List";
+
///
/// Apply an postfix to a
///
@@ -91,6 +96,6 @@ namespace Tgstation.Server.Api
///
/// The route
/// The with /List appended
- public static string List(string route) => String.Format(CultureInfo.InvariantCulture, "{0}/List", route);
+ public static string ListRoute(string route) => String.Format(CultureInfo.InvariantCulture, "{0}/{1}", route, List);
}
}
diff --git a/src/Tgstation.Server.Client/ApiClient.cs b/src/Tgstation.Server.Client/ApiClient.cs
index 3d01a67871..b6ae18e747 100644
--- a/src/Tgstation.Server.Client/ApiClient.cs
+++ b/src/Tgstation.Server.Client/ApiClient.cs
@@ -112,7 +112,11 @@ namespace Tgstation.Server.Client
throw new MethodNotSupportedException();
case HttpStatusCode.InternalServerError:
// response json is html
- throw new ServerErrorException(json);
+ throw new ServerErrorException(errorMessage ?? new ErrorMessage
+ {
+ Message = "An internal server error occurred!",
+ SeverApiVersion = null
+ }, response.StatusCode);
case (HttpStatusCode)429:
// rate limited
response.Headers.TryGetValues("Retry-After", out var values);
diff --git a/src/Tgstation.Server.Client/Components/ByondClient.cs b/src/Tgstation.Server.Client/Components/ByondClient.cs
index f3675dc265..23d56dc64a 100644
--- a/src/Tgstation.Server.Client/Components/ByondClient.cs
+++ b/src/Tgstation.Server.Client/Components/ByondClient.cs
@@ -35,7 +35,7 @@ namespace Tgstation.Server.Client.Components
public Task ActiveVersion(CancellationToken cancellationToken) => apiClient.Read(Routes.Byond, instance.Id, cancellationToken);
///
- public Task> InstalledVersions(CancellationToken cancellationToken) => apiClient.Read>(Routes.List(Routes.Byond), instance.Id, cancellationToken);
+ public Task> InstalledVersions(CancellationToken cancellationToken) => apiClient.Read>(Routes.ListRoute(Routes.Byond), instance.Id, cancellationToken);
///
public Task SetActiveVersion(Byond byond, CancellationToken cancellationToken) => apiClient.Update(Routes.Byond, byond ?? throw new ArgumentNullException(nameof(byond)), instance.Id, cancellationToken);
diff --git a/src/Tgstation.Server.Client/Components/ChatBotsClient.cs b/src/Tgstation.Server.Client/Components/ChatBotsClient.cs
index f7890f1f22..5a05e8c930 100644
--- a/src/Tgstation.Server.Client/Components/ChatBotsClient.cs
+++ b/src/Tgstation.Server.Client/Components/ChatBotsClient.cs
@@ -38,7 +38,7 @@ namespace Tgstation.Server.Client.Components
public Task Delete(ChatBot settings, CancellationToken cancellationToken) => apiClient.Delete(Routes.SetID(Routes.Chat, settings?.Id ?? throw new ArgumentNullException(nameof(settings))), instance.Id, cancellationToken);
///
- public Task> List(CancellationToken cancellationToken) => apiClient.Read>(Routes.List(Routes.Chat), instance.Id, cancellationToken);
+ public Task> List(CancellationToken cancellationToken) => apiClient.Read>(Routes.ListRoute(Routes.Chat), instance.Id, cancellationToken);
///
public Task Update(ChatBot settings, CancellationToken cancellationToken) => apiClient.Update(Routes.Chat, settings ?? throw new ArgumentNullException(nameof(settings)), instance.Id, cancellationToken);
diff --git a/src/Tgstation.Server.Client/Components/ConfigurationClient.cs b/src/Tgstation.Server.Client/Components/ConfigurationClient.cs
index 54dc6acf39..4063e73963 100644
--- a/src/Tgstation.Server.Client/Components/ConfigurationClient.cs
+++ b/src/Tgstation.Server.Client/Components/ConfigurationClient.cs
@@ -52,7 +52,7 @@ namespace Tgstation.Server.Client.Components
public Task CreateDirectory(ConfigurationFile directory, CancellationToken cancellationToken) => apiClient.Create(Routes.Configuration, directory, instance.Id, cancellationToken);
///
- public Task> List(string directory, CancellationToken cancellationToken) => apiClient.Read>(Routes.List(Routes.Configuration) + SanitizeGetPath(directory), instance.Id, cancellationToken);
+ public Task> List(string directory, CancellationToken cancellationToken) => apiClient.Read>(Routes.ListRoute(Routes.Configuration) + SanitizeGetPath(directory), instance.Id, cancellationToken);
///
public Task Read(ConfigurationFile file, CancellationToken cancellationToken)
diff --git a/src/Tgstation.Server.Client/Components/DreamMakerClient.cs b/src/Tgstation.Server.Client/Components/DreamMakerClient.cs
index 942d252812..dafa049834 100644
--- a/src/Tgstation.Server.Client/Components/DreamMakerClient.cs
+++ b/src/Tgstation.Server.Client/Components/DreamMakerClient.cs
@@ -35,10 +35,10 @@ namespace Tgstation.Server.Client.Components
public Task Compile(CancellationToken cancellationToken) => apiClient.Create(Routes.DreamMaker, instance.Id, cancellationToken);
///
- public Task GetCompileJob(CompileJob compileJob, CancellationToken cancellationToken) => apiClient.Read(Routes.SetID(Routes.DreamMaker, compileJob?.Id ?? throw new ArgumentNullException(nameof(compileJob))), instance.Id, cancellationToken);
+ public Task GetCompileJob(EntityId compileJob, CancellationToken cancellationToken) => apiClient.Read(Routes.SetID(Routes.DreamMaker, compileJob?.Id ?? throw new ArgumentNullException(nameof(compileJob))), instance.Id, cancellationToken);
///
- public Task> GetJobIds(CancellationToken cancellationToken) => apiClient.Read>(Routes.List(Routes.DreamMaker), instance.Id, cancellationToken);
+ public Task> GetJobIds(CancellationToken cancellationToken) => apiClient.Read>(Routes.ListRoute(Routes.DreamMaker), instance.Id, cancellationToken);
///
public Task Read(CancellationToken cancellationToken) => apiClient.Read(Routes.DreamMaker, instance.Id, cancellationToken);
diff --git a/src/Tgstation.Server.Client/Components/IDreamMakerClient.cs b/src/Tgstation.Server.Client/Components/IDreamMakerClient.cs
index 011475d5a5..174634bdb4 100644
--- a/src/Tgstation.Server.Client/Components/IDreamMakerClient.cs
+++ b/src/Tgstation.Server.Client/Components/IDreamMakerClient.cs
@@ -33,18 +33,18 @@ namespace Tgstation.Server.Client.Components
Task Compile(CancellationToken cancellationToken);
///
- /// Gets the s of all s for the instance
+ /// Gets the s of all s for the instance
///
/// The for the operation
- /// A resulting in a of s with only the field populated
- Task> GetJobIds(CancellationToken cancellationToken);
+ /// A resulting in a of s.
+ Task> GetJobIds(CancellationToken cancellationToken);
///
/// Get a
///
- /// The to get
+ /// The to get
/// The for the operation
/// A resulting in the
- Task GetCompileJob(CompileJob compileJob, CancellationToken cancellationToken);
+ Task GetCompileJob(EntityId compileJob, CancellationToken cancellationToken);
}
}
diff --git a/src/Tgstation.Server.Client/Components/IJobsClient.cs b/src/Tgstation.Server.Client/Components/IJobsClient.cs
index 8ce64a95e0..ae879f485e 100644
--- a/src/Tgstation.Server.Client/Components/IJobsClient.cs
+++ b/src/Tgstation.Server.Client/Components/IJobsClient.cs
@@ -1,5 +1,4 @@
-using System;
-using System.Collections.Generic;
+using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Tgstation.Server.Api.Models;
@@ -12,11 +11,11 @@ namespace Tgstation.Server.Client.Components
public interface IJobsClient
{
///
- /// List the s in the
+ /// List the s in the
///
/// The for the operation
- /// A resulting in a of the s in the
- Task> List(CancellationToken cancellationToken);
+ /// A resulting in a of the s in the
+ Task> List(CancellationToken cancellationToken);
///
/// List the active s in the
@@ -28,10 +27,10 @@ namespace Tgstation.Server.Client.Components
///
/// Get a
///
- /// The to get
+ /// The 's to get
/// The for the operation
/// A resulting in the
- Task GetId(Job job, CancellationToken cancellationToken);
+ Task GetId(EntityId job, CancellationToken cancellationToken);
///
/// Cancels a
diff --git a/src/Tgstation.Server.Client/Components/InstanceUserClient.cs b/src/Tgstation.Server.Client/Components/InstanceUserClient.cs
index 440340b6c4..cc9757f00b 100644
--- a/src/Tgstation.Server.Client/Components/InstanceUserClient.cs
+++ b/src/Tgstation.Server.Client/Components/InstanceUserClient.cs
@@ -44,7 +44,7 @@ namespace Tgstation.Server.Client.Components
public Task Update(InstanceUser instanceUser, CancellationToken cancellationToken) => apiClient.Update(Routes.InstanceUser, instanceUser ?? throw new ArgumentNullException(nameof(instanceUser)), instance.Id, cancellationToken);
///
- public Task> List(CancellationToken cancellationToken) => apiClient.Read>(Routes.List(Routes.InstanceUser), instance.Id, cancellationToken);
+ public Task> List(CancellationToken cancellationToken) => apiClient.Read>(Routes.ListRoute(Routes.InstanceUser), instance.Id, cancellationToken);
///
public Task GetId(InstanceUser instanceUser, CancellationToken cancellationToken) => apiClient.Read(Routes.SetID(Routes.InstanceUser, instanceUser?.UserId ?? throw new ArgumentNullException(nameof(instanceUser))), instance.Id, cancellationToken);
diff --git a/src/Tgstation.Server.Client/Components/JobsClient.cs b/src/Tgstation.Server.Client/Components/JobsClient.cs
index d6c421e65d..228a2c4a9b 100644
--- a/src/Tgstation.Server.Client/Components/JobsClient.cs
+++ b/src/Tgstation.Server.Client/Components/JobsClient.cs
@@ -35,12 +35,12 @@ namespace Tgstation.Server.Client.Components
public Task Cancel(Job job, CancellationToken cancellationToken) => apiClient.Delete(Routes.SetID(Routes.Jobs, job?.Id ?? throw new ArgumentNullException(nameof(job))), instance.Id, cancellationToken);
///
- public Task> List(CancellationToken cancellationToken) => apiClient.Read>(Routes.List(Routes.Jobs), instance.Id, cancellationToken);
+ public Task> List(CancellationToken cancellationToken) => apiClient.Read>(Routes.ListRoute(Routes.Jobs), instance.Id, cancellationToken);
///
public Task> ListActive(CancellationToken cancellationToken) => apiClient.Read>(Routes.Jobs, instance.Id, cancellationToken);
///
- public Task GetId(Job job, CancellationToken cancellationToken) => apiClient.Read(Routes.SetID(Routes.Jobs, job?.Id ?? throw new ArgumentNullException(nameof(job))), instance.Id, cancellationToken);
+ public Task GetId(EntityId job, CancellationToken cancellationToken) => apiClient.Read(Routes.SetID(Routes.Jobs, job?.Id ?? throw new ArgumentNullException(nameof(job))), instance.Id, cancellationToken);
}
}
\ No newline at end of file
diff --git a/src/Tgstation.Server.Client/InstanceManagerClient.cs b/src/Tgstation.Server.Client/InstanceManagerClient.cs
index 62d99de98d..aa5458f0e6 100644
--- a/src/Tgstation.Server.Client/InstanceManagerClient.cs
+++ b/src/Tgstation.Server.Client/InstanceManagerClient.cs
@@ -39,7 +39,7 @@ namespace Tgstation.Server.Client
public Task Detach(Instance instance, CancellationToken cancellationToken) => apiClient.Delete(Routes.SetID(Routes.InstanceManager, instance?.Id ?? throw new ArgumentNullException(nameof(instance))), cancellationToken);
///
- public Task> List(CancellationToken cancellationToken) => apiClient.Read>(Routes.List(Routes.InstanceManager), cancellationToken);
+ public Task> List(CancellationToken cancellationToken) => apiClient.Read>(Routes.ListRoute(Routes.InstanceManager), cancellationToken);
///
public Task Update(Instance instance, CancellationToken cancellationToken) => apiClient.Update(Routes.InstanceManager, instance ?? throw new ArgumentNullException(nameof(instance)), cancellationToken);
diff --git a/src/Tgstation.Server.Client/ServerErrorException.cs b/src/Tgstation.Server.Client/ServerErrorException.cs
index 940f3513a7..24794c8027 100644
--- a/src/Tgstation.Server.Client/ServerErrorException.cs
+++ b/src/Tgstation.Server.Client/ServerErrorException.cs
@@ -9,27 +9,18 @@ namespace Tgstation.Server.Client
///
public sealed class ServerErrorException : ClientException
{
- ///
- /// The raw HTML of the error
- ///
- public string Html { get; }
-
///
/// Construct an
///
public ServerErrorException() { }
///
- /// Construct an with
+ /// Construct an with a given
///
- /// The raw HTML response of the
- public ServerErrorException(string html) : base(new ErrorMessage
+ /// The for the
+ /// The for the
+ public ServerErrorException(ErrorMessage errorMessage, HttpStatusCode statusCode) : base(errorMessage, statusCode)
{
- Message = "An internal server error occurred!",
- SeverApiVersion = null
- }, HttpStatusCode.InternalServerError)
- {
- Html = html;
}
///
diff --git a/src/Tgstation.Server.Client/UsersClient.cs b/src/Tgstation.Server.Client/UsersClient.cs
index 1e6c5fd45e..9d4aa53de6 100644
--- a/src/Tgstation.Server.Client/UsersClient.cs
+++ b/src/Tgstation.Server.Client/UsersClient.cs
@@ -31,7 +31,7 @@ namespace Tgstation.Server.Client
public Task GetId(User user, CancellationToken cancellationToken) => apiClient.Read(Routes.SetID(Routes.User, user?.Id ?? throw new ArgumentNullException(nameof(user))), cancellationToken);
///
- public Task> List(CancellationToken cancellationToken) => apiClient.Read>(Routes.List(Routes.User), cancellationToken);
+ public Task> List(CancellationToken cancellationToken) => apiClient.Read>(Routes.ListRoute(Routes.User), cancellationToken);
///
public Task Read(CancellationToken cancellationToken) => apiClient.Read(Routes.User, cancellationToken);
diff --git a/src/Tgstation.Server.Host/Components/Byond/ByondInstallerBase.cs b/src/Tgstation.Server.Host/Components/Byond/ByondInstallerBase.cs
new file mode 100644
index 0000000000..7d7c98bf9c
--- /dev/null
+++ b/src/Tgstation.Server.Host/Components/Byond/ByondInstallerBase.cs
@@ -0,0 +1,64 @@
+using Microsoft.Extensions.Logging;
+using System;
+using System.Globalization;
+using System.Threading;
+using System.Threading.Tasks;
+using Tgstation.Server.Host.IO;
+
+namespace Tgstation.Server.Host.Components.Byond
+{
+ ///
+ abstract class ByondInstallerBase : IByondInstaller
+ {
+ ///
+ public abstract string DreamDaemonName { get; }
+
+ ///
+ public abstract string DreamMakerName { get; }
+
+ ///
+ /// Gets the URL formatter string for downloading a byond version of {0:Major} {1:Minor}.
+ ///
+ protected abstract string ByondRevisionsURLTemplate { get; }
+
+ ///
+ /// Gets the for the .
+ ///
+ protected IIOManager IOManager { get; }
+
+ ///
+ /// Gets the for the .
+ ///
+ protected ILogger Logger { get; }
+
+ ///
+ /// Initializes a new instance of the .
+ ///
+ /// The value of .
+ /// The value of .
+ protected ByondInstallerBase(IIOManager ioManager, ILogger logger)
+ {
+ IOManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager));
+ Logger = logger ?? throw new ArgumentNullException(nameof(logger));
+ }
+
+ ///
+ public abstract Task CleanCache(CancellationToken cancellationToken);
+
+ ///
+ public abstract Task InstallByond(string path, Version version, CancellationToken cancellationToken);
+
+ ///
+ public Task DownloadVersion(Version version, CancellationToken cancellationToken)
+ {
+ if (version == null)
+ throw new ArgumentNullException(nameof(version));
+
+ var url = String.Format(CultureInfo.InvariantCulture, ByondRevisionsURLTemplate, version.Major, version.Minor);
+
+ Logger.LogTrace("Downloading from: {0}", url);
+
+ return IOManager.DownloadFile(new Uri(url), cancellationToken);
+ }
+ }
+}
diff --git a/src/Tgstation.Server.Host/Components/Byond/PosixByondInstaller.cs b/src/Tgstation.Server.Host/Components/Byond/PosixByondInstaller.cs
index 6d741bb952..8b42a813ee 100644
--- a/src/Tgstation.Server.Host/Components/Byond/PosixByondInstaller.cs
+++ b/src/Tgstation.Server.Host/Components/Byond/PosixByondInstaller.cs
@@ -11,13 +11,8 @@ namespace Tgstation.Server.Host.Components.Byond
///
/// for Posix systems
///
- sealed class PosixByondInstaller : IByondInstaller
+ sealed class PosixByondInstaller : ByondInstallerBase
{
- ///
- /// The URL format string for getting BYOND linux version {0}.{1} zipfile
- ///
- const string ByondRevisionsURLTemplate = "https://secure.byond.com/download/build/{0}/{0}.{1}_byond_linux.zip";
-
///
/// Path to the BYOND cache
///
@@ -28,45 +23,37 @@ namespace Tgstation.Server.Host.Components.Byond
const string ShellScriptExtension = ".sh";
///
- public string DreamDaemonName => DreamDaemonExecutableName + ShellScriptExtension;
+ public override string DreamDaemonName => DreamDaemonExecutableName + ShellScriptExtension;
///
- public string DreamMakerName => DreamMakerExecutableName + ShellScriptExtension;
+ public override string DreamMakerName => DreamMakerExecutableName + ShellScriptExtension;
- ///
- /// The for the
- ///
- readonly IIOManager ioManager;
+ ///
+ protected override string ByondRevisionsURLTemplate => "https://secure.byond.com/download/build/{0}/{0}.{1}_byond_linux.zip";
///
/// The for the
///
readonly IPostWriteHandler postWriteHandler;
- ///
- /// The for the
- ///
- readonly ILogger logger;
-
///
/// Construct a
///
- /// The value of
/// The value of
- /// The value of
- public PosixByondInstaller(IIOManager ioManager, IPostWriteHandler postWriteHandler, ILogger logger)
+ /// The for the .
+ /// The for the .
+ public PosixByondInstaller(IPostWriteHandler postWriteHandler, IIOManager ioManager, ILogger logger)
+ : base(ioManager, logger)
{
- this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager));
this.postWriteHandler = postWriteHandler ?? throw new ArgumentNullException(nameof(postWriteHandler));
- this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
///
- public async Task CleanCache(CancellationToken cancellationToken)
+ public override async Task CleanCache(CancellationToken cancellationToken)
{
try
{
- await ioManager.DeleteDirectory(ByondCachePath, cancellationToken).ConfigureAwait(false);
+ await IOManager.DeleteDirectory(ByondCachePath, cancellationToken).ConfigureAwait(false);
}
catch (OperationCanceledException)
{
@@ -74,23 +61,12 @@ namespace Tgstation.Server.Host.Components.Byond
}
catch (Exception e)
{
- logger.LogWarning("Error deleting BYOND cache! Exception: {0}", e);
+ Logger.LogWarning("Error deleting BYOND cache! Exception: {0}", e);
}
}
///
- public async Task DownloadVersion(Version version, CancellationToken cancellationToken)
- {
- if (version == null)
- throw new ArgumentNullException(nameof(version));
-
- var url = String.Format(CultureInfo.InvariantCulture, ByondRevisionsURLTemplate, version.Major, version.Minor);
-
- return await ioManager.DownloadFile(new Uri(url), cancellationToken).ConfigureAwait(false);
- }
-
- ///
- public Task InstallByond(string path, Version version, CancellationToken cancellationToken)
+ public override Task InstallByond(string path, Version version, CancellationToken cancellationToken)
{
if (path == null)
throw new ArgumentNullException(nameof(path));
@@ -106,16 +82,16 @@ namespace Tgstation.Server.Host.Components.Byond
async Task WriteAndMakeExecutable(string fullPath, string script)
{
- await ioManager.WriteAllBytes(fullPath, Encoding.ASCII.GetBytes(script), cancellationToken).ConfigureAwait(false);
+ await IOManager.WriteAllBytes(fullPath, Encoding.ASCII.GetBytes(script), cancellationToken).ConfigureAwait(false);
postWriteHandler.HandleWrite(fullPath);
}
- var basePath = ioManager.ConcatPath(path, ByondManager.BinPath);
+ var basePath = IOManager.ConcatPath(path, ByondManager.BinPath);
- var task = Task.WhenAll(WriteAndMakeExecutable(ioManager.ConcatPath(basePath, DreamDaemonName), dreamDaemonScript), WriteAndMakeExecutable(ioManager.ConcatPath(basePath, DreamMakerName), dreamMakerScript));
+ var task = Task.WhenAll(WriteAndMakeExecutable(IOManager.ConcatPath(basePath, DreamDaemonName), dreamDaemonScript), WriteAndMakeExecutable(IOManager.ConcatPath(basePath, DreamMakerName), dreamMakerScript));
- postWriteHandler.HandleWrite(ioManager.ConcatPath(basePath, DreamDaemonExecutableName));
- postWriteHandler.HandleWrite(ioManager.ConcatPath(basePath, DreamMakerExecutableName));
+ postWriteHandler.HandleWrite(IOManager.ConcatPath(basePath, DreamDaemonExecutableName));
+ postWriteHandler.HandleWrite(IOManager.ConcatPath(basePath, DreamMakerExecutableName));
return task;
}
diff --git a/src/Tgstation.Server.Host/Components/Byond/WindowsByondInstaller.cs b/src/Tgstation.Server.Host/Components/Byond/WindowsByondInstaller.cs
index 801b0468fc..4b48d6f0bf 100644
--- a/src/Tgstation.Server.Host/Components/Byond/WindowsByondInstaller.cs
+++ b/src/Tgstation.Server.Host/Components/Byond/WindowsByondInstaller.cs
@@ -12,13 +12,8 @@ namespace Tgstation.Server.Host.Components.Byond
///
/// for windows systems
///
- sealed class WindowsByondInstaller : IByondInstaller, IDisposable
+ sealed class WindowsByondInstaller : ByondInstallerBase, IDisposable
{
- ///
- /// The URL format string for getting BYOND windows version {0}.{1} zipfile
- ///
- const string ByondRevisionsURLTemplate = "https://secure.byond.com/download/build/{0}/{0}.{1}_byond.zip";
-
///
/// Directory to byond installation configuration
///
@@ -40,26 +35,19 @@ namespace Tgstation.Server.Host.Components.Byond
const string ByondDXDir = "byond/directx";
///
- public string DreamDaemonName => "dreamdaemon.exe";
+ public override string DreamDaemonName => "dreamdaemon.exe";
///
- public string DreamMakerName => "dm.exe";
+ public override string DreamMakerName => "dm.exe";
- ///
- /// The for the
- ///
- readonly IIOManager ioManager;
+ ///
+ protected override string ByondRevisionsURLTemplate => "https://secure.byond.com/download/build/{0}/{0}.{1}_byond.zip";
///
/// The for the
///
readonly IProcessExecutor processExecutor;
- ///
- /// The for the
- ///
- readonly ILogger logger;
-
///
/// The for the
///
@@ -73,14 +61,13 @@ namespace Tgstation.Server.Host.Components.Byond
///
/// Construct a
///
- /// The value of
/// The value of
- /// The value of
- public WindowsByondInstaller(IIOManager ioManager, IProcessExecutor processExecutor, ILogger logger)
+ /// The for the .
+ /// The for the .
+ public WindowsByondInstaller(IProcessExecutor processExecutor, IIOManager ioManager, ILogger logger)
+ : base(ioManager, logger)
{
- this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager));
this.processExecutor = processExecutor ?? throw new ArgumentNullException(nameof(processExecutor));
- this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
semaphore = new SemaphoreSlim(1);
installedDirectX = false;
@@ -90,11 +77,11 @@ namespace Tgstation.Server.Host.Components.Byond
public void Dispose() => semaphore.Dispose();
///
- public async Task CleanCache(CancellationToken cancellationToken)
+ public override async Task CleanCache(CancellationToken cancellationToken)
{
try
{
- await ioManager.DeleteDirectory(ioManager.ConcatPath(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), "byond/cache"), cancellationToken).ConfigureAwait(false);
+ await IOManager.DeleteDirectory(IOManager.ConcatPath(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), "byond/cache"), cancellationToken).ConfigureAwait(false);
}
catch(OperationCanceledException)
{
@@ -102,26 +89,18 @@ namespace Tgstation.Server.Host.Components.Byond
}
catch (Exception e)
{
- logger.LogWarning("Error deleting BYOND cache! Exception: {0}", e);
+ Logger.LogWarning("Error deleting BYOND cache! Exception: {0}", e);
}
}
///
- public Task DownloadVersion(Version version, CancellationToken cancellationToken)
- {
- var url = String.Format(CultureInfo.InvariantCulture, ByondRevisionsURLTemplate, version.Major, version.Minor);
-
- return ioManager.DownloadFile(new Uri(url), cancellationToken);
- }
-
- ///
- public async Task InstallByond(string path, Version version, CancellationToken cancellationToken)
+ public override async Task InstallByond(string path, Version version, CancellationToken cancellationToken)
{
async Task SetNoPromptTrusted()
{
- var configPath = ioManager.ConcatPath(path, ByondConfigDir);
- await ioManager.CreateDirectory(configPath, cancellationToken).ConfigureAwait(false);
- await ioManager.WriteAllBytes(ioManager.ConcatPath(configPath, ByondDDConfig), Encoding.UTF8.GetBytes(ByondNoPromptTrustedMode), cancellationToken).ConfigureAwait(false);
+ var configPath = IOManager.ConcatPath(path, ByondConfigDir);
+ await IOManager.CreateDirectory(configPath, cancellationToken).ConfigureAwait(false);
+ await IOManager.WriteAllBytes(IOManager.ConcatPath(configPath, ByondDDConfig), Encoding.UTF8.GetBytes(ByondNoPromptTrustedMode), cancellationToken).ConfigureAwait(false);
}
var setNoPromptTrustedModeTask = SetNoPromptTrusted();
@@ -135,13 +114,13 @@ namespace Tgstation.Server.Host.Components.Byond
{
// ^check again because race conditions
// always install it, it's pretty fast and will do better redundancy checking than us
- var rbdx = ioManager.ConcatPath(path, ByondDXDir);
+ var rbdx = IOManager.ConcatPath(path, ByondDXDir);
// noShellExecute because we aren't doing runas shennanigans
IProcess directXInstaller;
try
{
- directXInstaller = processExecutor.LaunchProcess(ioManager.ConcatPath(rbdx, "DXSETUP.exe"), rbdx, "/silent", noShellExecute: true);
+ directXInstaller = processExecutor.LaunchProcess(IOManager.ConcatPath(rbdx, "DXSETUP.exe"), rbdx, "/silent", noShellExecute: true);
}
catch (Exception e)
{
diff --git a/src/Tgstation.Server.Host/Controllers/AdministrationController.cs b/src/Tgstation.Server.Host/Controllers/AdministrationController.cs
index 61198121f6..def822cfb8 100644
--- a/src/Tgstation.Server.Host/Controllers/AdministrationController.cs
+++ b/src/Tgstation.Server.Host/Controllers/AdministrationController.cs
@@ -22,10 +22,10 @@ using Tgstation.Server.Host.Security;
namespace Tgstation.Server.Host.Controllers
{
///
- /// for
+ /// for purposes
///
[Route(Routes.Administration)]
- public sealed class AdministrationController : ModelController
+ public sealed class AdministrationController : ApiController
{
const string RestartNotSupportedException = "This deployment of tgstation-server is lacking the Tgstation.Server.Host.Watchdog component. Restarts and version changes cannot be completed!";
@@ -79,7 +79,7 @@ namespace Tgstation.Server.Host.Controllers
/// The for the
/// The containing value of
/// The containing value of
- public AdministrationController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, IGitHubClientFactory gitHubClientFactory, IServerControl serverUpdater, IApplication application, IIOManager ioManager, IPlatformIdentifier platformIdentifier, ILogger logger, IOptions updatesConfigurationOptions, IOptions generalConfigurationOptions) : base(databaseContext, authenticationContextFactory, logger, false)
+ public AdministrationController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, IGitHubClientFactory gitHubClientFactory, IServerControl serverUpdater, IApplication application, IIOManager ioManager, IPlatformIdentifier platformIdentifier, ILogger logger, IOptions updatesConfigurationOptions, IOptions generalConfigurationOptions) : base(databaseContext, authenticationContextFactory, logger, false, true)
{
this.gitHubClientFactory = gitHubClientFactory ?? throw new ArgumentNullException(nameof(gitHubClientFactory));
this.serverUpdater = serverUpdater ?? throw new ArgumentNullException(nameof(serverUpdater));
@@ -152,9 +152,19 @@ namespace Tgstation.Server.Host.Controllers
IGitHubClient GetGitHubClient() => String.IsNullOrEmpty(generalConfiguration.GitHubAccessToken) ? gitHubClientFactory.CreateClient() : gitHubClientFactory.CreateClient(generalConfiguration.GitHubAccessToken);
- ///
+ ///
+ /// Get server information.
+ ///
+ /// A resulting in the for the operation.
+ /// Retrieved data successfully.
+ /// The GitHub API rate limit was hit. See response header Retry-After.
+ /// A GitHub API error occurred. See error message for details.
+ [HttpGet]
[TgsAuthorize]
- public override async Task Read(CancellationToken cancellationToken)
+ [ProducesResponseType(typeof(Administration), 200)]
+ [ProducesResponseType(424)]
+ [ProducesResponseType(typeof(ErrorMessage), 429)]
+ public async Task Read()
{
try
{
@@ -192,13 +202,24 @@ namespace Tgstation.Server.Host.Controllers
catch (ApiException e)
{
Logger.LogWarning(OctokitException, e);
- return StatusCode((int)HttpStatusCode.FailedDependency);
+ return StatusCode((int)HttpStatusCode.FailedDependency, new ErrorMessage
+ {
+ Message = e.Message
+ });
}
}
- ///
+ ///
+ /// Attempt to perform a server upgrade.
+ ///
+ /// The model containing the to update to.
+ /// The for the operation.
+ /// A resulting in the for the operation.
+ /// Upgrade operations are unavailable due to the launch configuration of TGS.
+ [HttpPost]
[TgsAuthorize(AdministrationRights.ChangeVersion)]
- public override async Task Update([FromBody] Administration model, CancellationToken cancellationToken)
+ [ProducesResponseType(typeof(ErrorMessage), 422)]
+ public async Task Update([FromBody] Administration model, CancellationToken cancellationToken)
{
if (model == null)
throw new ArgumentNullException(nameof(model));
@@ -222,8 +243,12 @@ namespace Tgstation.Server.Host.Controllers
/// Attempts to restart the server
///
/// A resulting in the of the request
- [HttpDelete]
+ /// Restart begun successfully.
+ /// Restart operations are unavailable due to the launch configuration of TGS.
+ [HttpDelete("{id}")]
[TgsAuthorize(AdministrationRights.RestartHost)]
+ [ProducesResponseType(200)]
+ [ProducesResponseType(typeof(ErrorMessage), 422)]
public async Task Delete()
{
try
diff --git a/src/Tgstation.Server.Host/Controllers/ApiController.cs b/src/Tgstation.Server.Host/Controllers/ApiController.cs
index 9565072343..c5804466aa 100644
--- a/src/Tgstation.Server.Host/Controllers/ApiController.cs
+++ b/src/Tgstation.Server.Host/Controllers/ApiController.cs
@@ -18,6 +18,7 @@ namespace Tgstation.Server.Host.Controllers
/// A for API functions
///
[Produces(ApiHeaders.ApplicationJson)]
+ [ApiController]
public abstract class ApiController : Controller
{
///
@@ -129,25 +130,9 @@ namespace Tgstation.Server.Host.Controllers
if (ModelState?.IsValid == false)
{
- var errorMessages = ModelState.SelectMany(x => x.Value.Errors).Select(x => x.ErrorMessage).ToList();
-
- // HACK
- // do some fuckery to remove RequiredAttribute errors
- for (var I = 0; I < errorMessages.Count; ++I)
- {
- var message = errorMessages[I];
- if (message.StartsWith("The ", StringComparison.Ordinal) && message.EndsWith(" field is required.", StringComparison.Ordinal))
- {
- errorMessages.RemoveAt(I);
- --I;
- }
- }
-
- if (errorMessages.Count > 0)
- {
- await BadRequest(new ErrorMessage { Message = String.Join(Environment.NewLine, errorMessages) }).ExecuteResultAsync(context).ConfigureAwait(false);
- return;
- }
+ var errorMessages = ModelState.SelectMany(x => x.Value.Errors).Select(x => x.ErrorMessage);
+ await BadRequest(new ErrorMessage { Message = String.Join(Environment.NewLine, errorMessages) }).ExecuteResultAsync(context).ConfigureAwait(false);
+ return;
}
if (ApiHeaders != null)
diff --git a/src/Tgstation.Server.Host/Controllers/ByondController.cs b/src/Tgstation.Server.Host/Controllers/ByondController.cs
index 10229aaef1..52872441b6 100644
--- a/src/Tgstation.Server.Host/Controllers/ByondController.cs
+++ b/src/Tgstation.Server.Host/Controllers/ByondController.cs
@@ -1,7 +1,7 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using System;
-using System.Globalization;
+using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
@@ -19,7 +19,7 @@ namespace Tgstation.Server.Host.Controllers
/// Controller for managing s
///
[Route(Routes.Byond)]
- public sealed class ByondController : ModelController
+ public sealed class ByondController : ApiController
{
///
/// The for the
@@ -39,31 +39,53 @@ namespace Tgstation.Server.Host.Controllers
/// The value of
/// The value of
/// The for the
- public ByondController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, IInstanceManager instanceManager, IJobManager jobManager, ILogger logger) : base(databaseContext, authenticationContextFactory, logger, true)
+ public ByondController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, IInstanceManager instanceManager, IJobManager jobManager, ILogger logger) : base(databaseContext, authenticationContextFactory, logger, true, true)
{
this.instanceManager = instanceManager ?? throw new ArgumentNullException(nameof(instanceManager));
this.jobManager = jobManager ?? throw new ArgumentNullException(nameof(jobManager));
}
- ///
+ ///
+ /// Gets the active version.
+ ///
+ /// A resulting in the for the operation.
+ /// Retrieved version information successfully.
+ [HttpGet]
[TgsAuthorize(ByondRights.ReadActive)]
- public override Task Read(CancellationToken cancellationToken) => Task.FromResult(
+ [ProducesResponseType(typeof(Api.Models.Byond), 200)]
+ public Task Read() => Task.FromResult(
Json(new Api.Models.Byond
{
Version = instanceManager.GetInstance(Instance).ByondManager.ActiveVersion
}));
- ///
+ ///
+ /// Lists installed versions.
+ ///
+ /// A resulting in the for the operation.
+ /// Retrieved version information successfully.
+ [HttpGet(Routes.List)]
[TgsAuthorize(ByondRights.ListInstalled)]
- public override Task List(CancellationToken cancellationToken) => Task.FromResult(
+ [ProducesResponseType(typeof(IEnumerable), 200)]
+ public Task List() => Task.FromResult(
Json(instanceManager.GetInstance(Instance).ByondManager.InstalledVersions.Select(x => new Api.Models.Byond
{
Version = x
})));
- ///
+ ///
+ /// Changes the active BYOND version to the one specified in a given .
+ ///
+ /// The to switch to.
+ /// The for the operation.
+ /// A resulting in the for the operation.
+ /// Switched active version successfully.
+ /// Created to install and switch active version successfully.
+ [HttpPost]
[TgsAuthorize(ByondRights.ChangeVersion)]
- public override async Task Update([FromBody] Api.Models.Byond model, CancellationToken cancellationToken)
+ [ProducesResponseType(typeof(Api.Models.Byond), 200)]
+ [ProducesResponseType(typeof(Api.Models.Byond), 202)]
+ public async Task Update([FromBody] Api.Models.Byond model, CancellationToken cancellationToken)
{
if (model == null)
throw new ArgumentNullException(nameof(model));
@@ -90,7 +112,7 @@ namespace Tgstation.Server.Host.Controllers
// run the install through the job manager
var job = new Models.Job
{
- Description = String.Format(CultureInfo.InvariantCulture, "Install BYOND version {0}", installingVersion),
+ Description = $"Install BYOND version {installingVersion}",
StartedBy = AuthenticationContext.User,
CancelRightsType = RightsType.Byond,
CancelRight = (ulong)ByondRights.CancelInstall,
diff --git a/src/Tgstation.Server.Host/Controllers/ChatController.cs b/src/Tgstation.Server.Host/Controllers/ChatController.cs
index 26ac8bc834..bbc3ba1f97 100644
--- a/src/Tgstation.Server.Host/Controllers/ChatController.cs
+++ b/src/Tgstation.Server.Host/Controllers/ChatController.cs
@@ -20,10 +20,10 @@ using Z.EntityFramework.Plus;
namespace Tgstation.Server.Host.Controllers
{
///
- /// for managing s
+ /// for managing s
///
[Route(Routes.Chat)]
- public sealed class ChatController : ModelController
+ public sealed class ChatController : ApiController
{
///
/// The for the
@@ -37,7 +37,7 @@ namespace Tgstation.Server.Host.Controllers
/// The for the
/// The value of
/// The for the
- public ChatController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, IInstanceManager instanceManager, ILogger logger) : base(databaseContext, authenticationContextFactory, logger, true)
+ public ChatController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, IInstanceManager instanceManager, ILogger logger) : base(databaseContext, authenticationContextFactory, logger, true, true)
{
this.instanceManager = instanceManager ?? throw new ArgumentNullException(nameof(instanceManager));
}
@@ -57,9 +57,17 @@ namespace Tgstation.Server.Host.Controllers
Tag = api.Tag
};
- ///
+ ///
+ /// Create a new chat bot .
+ ///
+ /// The to create.
+ /// The for the operation.
+ /// A resulting in the for the operation.
+ /// Created chat bot successfully.
+ [HttpPut]
[TgsAuthorize(ChatBotRights.Create)]
- public override async Task Create([FromBody] Api.Models.ChatBot model, CancellationToken cancellationToken)
+ [ProducesResponseType(typeof(Api.Models.ChatBot), 201)]
+ public async Task Create([FromBody] Api.Models.ChatBot model, CancellationToken cancellationToken)
{
if (model == null)
throw new ArgumentNullException(nameof(model));
@@ -129,9 +137,17 @@ namespace Tgstation.Server.Host.Controllers
return StatusCode((int)HttpStatusCode.Created, dbModel.ToApi());
}
- ///
+ ///
+ /// Delete a .
+ ///
+ /// The to delete.
+ /// The for the operation.
+ /// A resulting in the for the operation.
+ /// Chat bot deleted or does not exist.
+ [HttpDelete("{id}")]
[TgsAuthorize(ChatBotRights.Delete)]
- public override async Task Delete(long id, CancellationToken cancellationToken)
+ [ProducesResponseType(200)]
+ public async Task Delete(long id, CancellationToken cancellationToken)
{
var instance = instanceManager.GetInstance(Instance);
await Task.WhenAll(instance.Chat.DeleteConnection(id, cancellationToken), DatabaseContext.ChatBots.Where(x => x.Id == id).DeleteAsync(cancellationToken)).ConfigureAwait(false);
@@ -139,9 +155,16 @@ namespace Tgstation.Server.Host.Controllers
return Ok();
}
- ///
+ ///
+ /// List s.
+ ///
+ /// The for the operation.
+ /// A resulting in the for the operation.
+ /// Listed chat bots successfully.
+ [HttpGet(Routes.List)]
[TgsAuthorize(ChatBotRights.Read)]
- public override async Task List(CancellationToken cancellationToken)
+ [ProducesResponseType(typeof(IEnumerable), 200)]
+ public async Task List(CancellationToken cancellationToken)
{
var query = DatabaseContext.ChatBots.Where(x => x.InstanceId == Instance.Id).Include(x => x.Channels);
@@ -156,9 +179,19 @@ namespace Tgstation.Server.Host.Controllers
return Json(results.Select(x => x.ToApi()));
}
- ///
+ ///
+ /// Get a specific .
+ ///
+ /// The to retrieve.
+ /// The for the operation.
+ /// A resulting in the for the operation.
+ /// Retrieved successfully.
+ /// Chat bot does not exist.
+ [HttpGet("{id}")]
[TgsAuthorize(ChatBotRights.Read)]
- public override async Task GetId(long id, CancellationToken cancellationToken)
+ [ProducesResponseType(typeof(Api.Models.ChatBot), 200)]
+ [ProducesResponseType(410)]
+ public async Task GetId(long id, CancellationToken cancellationToken)
{
var query = DatabaseContext.ChatBots.Where(x => x.Id == id).Include(x => x.Channels);
@@ -174,10 +207,19 @@ namespace Tgstation.Server.Host.Controllers
return Json(results.ToApi());
}
- ///
- #pragma warning disable CA1506 // TODO: Decomplexify
+ ///
+ /// Updates a chat bot .
+ ///
+ /// The update to apply.
+ /// The for the operation.
+ /// A resulting in the for the operation.
+ /// Update applied successfully. may or may not be returned based on user permissions.
+ [HttpPost]
[TgsAuthorize(ChatBotRights.WriteChannels | ChatBotRights.WriteConnectionString | ChatBotRights.WriteEnabled | ChatBotRights.WriteName | ChatBotRights.WriteProvider)]
- public override async Task Update([FromBody] Api.Models.ChatBot model, CancellationToken cancellationToken)
+ [ProducesResponseType(200)]
+ [ProducesResponseType(typeof(Api.Models.ChatBot), 200)]
+ #pragma warning disable CA1506 // TODO: Decomplexify
+ public async Task Update([FromBody] Api.Models.ChatBot model, CancellationToken cancellationToken)
{
if (model == null)
throw new ArgumentNullException(nameof(model));
diff --git a/src/Tgstation.Server.Host/Controllers/ConfigurationController.cs b/src/Tgstation.Server.Host/Controllers/ConfigurationController.cs
index 49bbb78e67..363be52b3a 100644
--- a/src/Tgstation.Server.Host/Controllers/ConfigurationController.cs
+++ b/src/Tgstation.Server.Host/Controllers/ConfigurationController.cs
@@ -1,6 +1,7 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using System;
+using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Threading;
@@ -16,10 +17,10 @@ using Tgstation.Server.Host.Security;
namespace Tgstation.Server.Host.Controllers
{
///
- /// The for s
+ /// The for s
///
[Route(Routes.Configuration)]
- public sealed class ConfigurationController : ModelController
+ public sealed class ConfigurationController : ApiController
{
///
/// The for the
@@ -39,7 +40,7 @@ namespace Tgstation.Server.Host.Controllers
/// The value of
/// The value of
/// The for the
- public ConfigurationController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, IInstanceManager instanceManager, IIOManager ioManager, ILogger logger) : base(databaseContext, authenticationContextFactory, logger, true)
+ public ConfigurationController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, IInstanceManager instanceManager, IIOManager ioManager, ILogger logger) : base(databaseContext, authenticationContextFactory, logger, true, true)
{
this.instanceManager = instanceManager ?? throw new ArgumentNullException(nameof(instanceManager));
this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager));
@@ -63,9 +64,21 @@ namespace Tgstation.Server.Host.Controllers
return false;
}
- ///
+ ///
+ /// Write to a configuration file.
+ ///
+ /// The representing the file.
+ /// The for the operation.
+ /// A resulting in the for the operation.
+ /// File updated successfully.
+ /// File created successfully.
+ /// POSIX system impersonation requested but not implemented.
+ [HttpPost]
[TgsAuthorize(ConfigurationRights.Write)]
- public override async Task Update([FromBody] ConfigurationFile model, CancellationToken cancellationToken)
+ [ProducesResponseType(typeof(ConfigurationFile), 200)]
+ [ProducesResponseType(typeof(ConfigurationFile), 201)]
+ [ProducesResponseType(501)]
+ public async Task Update([FromBody] ConfigurationFile model, CancellationToken cancellationToken)
{
if (model == null)
throw new ArgumentNullException(nameof(model));
@@ -106,8 +119,13 @@ namespace Tgstation.Server.Host.Controllers
/// The path of the file to get
/// The for the operation
/// A resulting in the for the operation
+ /// File not found on disk.
+ /// POSIX system impersonation requested but not implemented.
[HttpGet(Routes.File + "/{*filePath}")]
[TgsAuthorize(ConfigurationRights.Read)]
+ [ProducesResponseType(typeof(ConfigurationFile), 200)]
+ [ProducesResponseType(410)]
+ [ProducesResponseType(501)]
public async Task File(string filePath, CancellationToken cancellationToken)
{
if (ForbidDueToModeConflicts(filePath, out var systemIdentity))
@@ -141,8 +159,13 @@ namespace Tgstation.Server.Host.Controllers
/// The path of the directory to get
/// The for the operation
/// A resulting in the for the operation
- [HttpGet("List/{*directoryPath}")]
+ /// Directory not found on disk.
+ /// POSIX system impersonation requested but not implemented.
+ [HttpGet(Routes.List + "/{*directoryPath}")]
[TgsAuthorize(ConfigurationRights.List)]
+ [ProducesResponseType(typeof(IReadOnlyList), 200)]
+ [ProducesResponseType(410)]
+ [ProducesResponseType(501)]
public async Task Directory(string directoryPath, CancellationToken cancellationToken)
{
if (ForbidDueToModeConflicts(directoryPath, out var systemIdentity))
@@ -166,13 +189,35 @@ namespace Tgstation.Server.Host.Controllers
}
}
- ///
+ ///
+ /// Get the contents of the root configuration directory.
+ ///
+ /// The for the operation.
+ /// A resulting in the for the operation.
+ /// Directory not found on disk.
+ /// POSIX system impersonation requested but not implemented.
+ [HttpGet(Routes.List)]
[TgsAuthorize(ConfigurationRights.List)]
- public override Task List(CancellationToken cancellationToken) => Directory(null, cancellationToken);
+ [ProducesResponseType(typeof(IReadOnlyList), 200)]
+ [ProducesResponseType(410)]
+ [ProducesResponseType(501)]
+ public Task List(CancellationToken cancellationToken) => Directory(null, cancellationToken);
- ///
+ ///
+ /// Create a configuration directory.
+ ///
+ /// The representing the directory.
+ /// The for the operation.
+ /// A resulting in the for the operation.
+ /// Directory already exists.
+ /// Directory created successfully.
+ /// POSIX system impersonation requested but not implemented.
+ [HttpPut]
[TgsAuthorize(ConfigurationRights.Write)]
- public override async Task Create([FromBody] ConfigurationFile model, CancellationToken cancellationToken)
+ [ProducesResponseType(typeof(ConfigurationFile), 200)]
+ [ProducesResponseType(typeof(ConfigurationFile), 201)]
+ [ProducesResponseType(501)]
+ public async Task Create([FromBody] ConfigurationFile model, CancellationToken cancellationToken)
{
if (model == null)
throw new ArgumentNullException(nameof(model));
@@ -185,6 +230,14 @@ namespace Tgstation.Server.Host.Controllers
model.IsDirectory = true;
return await instanceManager.GetInstance(Instance).Configuration.CreateDirectory(model.Path, systemIdentity, cancellationToken).ConfigureAwait(false) ? (IActionResult)Json(model) : StatusCode((int)HttpStatusCode.Created, model);
}
+ catch (IOException e)
+ {
+ Logger.LogInformation("IOException while creating directory {0}: {1}", model.Path, e);
+ return Conflict(new ErrorMessage
+ {
+ Message = e.Message
+ });
+ }
catch (NotImplementedException)
{
return StatusCode((int)HttpStatusCode.NotImplemented);
@@ -201,8 +254,12 @@ namespace Tgstation.Server.Host.Controllers
/// A representing the path to the directory to delete
/// The for the operation
/// A resulting in the of the operation
+ /// Empty directory deleted successfully.
+ /// POSIX system impersonation requested but not implemented.
[HttpDelete]
[TgsAuthorize(ConfigurationRights.Delete)]
+ [ProducesResponseType(200)]
+ [ProducesResponseType(501)]
public async Task Delete([FromBody] ConfigurationFile directory, CancellationToken cancellationToken)
{
if (directory == null)
diff --git a/src/Tgstation.Server.Host/Controllers/DreamDaemonController.cs b/src/Tgstation.Server.Host/Controllers/DreamDaemonController.cs
index 3a33d10457..d9d8324e25 100644
--- a/src/Tgstation.Server.Host/Controllers/DreamDaemonController.cs
+++ b/src/Tgstation.Server.Host/Controllers/DreamDaemonController.cs
@@ -20,10 +20,10 @@ using Tgstation.Server.Host.Security;
namespace Tgstation.Server.Host.Controllers
{
///
- /// for managing the
+ /// for managing the
///
[Route(Routes.DreamDaemon)]
- public sealed class DreamDaemonController : ModelController
+ public sealed class DreamDaemonController : ApiController
{
///
/// The for the
@@ -43,15 +43,24 @@ namespace Tgstation.Server.Host.Controllers
/// The value of
/// The value of
/// The for the
- public DreamDaemonController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, IJobManager jobManager, IInstanceManager instanceManager, ILogger logger) : base(databaseContext, authenticationContextFactory, logger, true)
+ public DreamDaemonController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, IJobManager jobManager, IInstanceManager instanceManager, ILogger logger) : base(databaseContext, authenticationContextFactory, logger, true, true)
{
this.jobManager = jobManager ?? throw new ArgumentNullException(nameof(jobManager));
this.instanceManager = instanceManager ?? throw new ArgumentNullException(nameof(instanceManager));
}
- ///
+ ///
+ /// Launches the watchdog.
+ ///
+ /// The for the operation.
+ /// A resulting in the of the operation.
+ /// to launch the watchdog started successfully.
+ /// Watchdog already running.
+ [HttpPut]
[TgsAuthorize(DreamDaemonRights.Start)]
- public override async Task Create([FromBody] DreamDaemon model, CancellationToken cancellationToken)
+ [ProducesResponseType(typeof(Api.Models.Job), 202)]
+ [ProducesResponseType(410)]
+ public async Task Create(CancellationToken cancellationToken)
{
// alias for launching DD
var instance = instanceManager.GetInstance(Instance);
@@ -71,9 +80,17 @@ namespace Tgstation.Server.Host.Controllers
return Accepted(job.ToApi());
}
- ///
+ ///
+ /// Get the watchdog status.
+ ///
+ /// The for the operation.
+ /// A resulting in the of the operation.
+ /// Read information successfully.
+ [HttpGet]
[TgsAuthorize(DreamDaemonRights.ReadMetadata | DreamDaemonRights.ReadRevision)]
- public override Task Read(CancellationToken cancellationToken) => ReadImpl(null, cancellationToken);
+ [ProducesResponseType(typeof(DreamDaemon), 200)]
+ [ProducesResponseType(410)]
+ public Task Read(CancellationToken cancellationToken) => ReadImpl(null, cancellationToken);
///
/// Implementation of
@@ -128,12 +145,14 @@ namespace Tgstation.Server.Host.Controllers
}
///
- /// Stops DreamDaemon if it's running
+ /// Stops the Watchdog if it's running.
///
- /// The for the operation
- /// A resulting in the of the operation
- [HttpDelete]
+ /// The for the operation.
+ /// A resulting in the of the operation.
+ /// Watchdog terminated.
+ [HttpDelete("{id}")]
[TgsAuthorize(DreamDaemonRights.Shutdown)]
+ [ProducesResponseType(200)]
public async Task Delete(CancellationToken cancellationToken)
{
var instance = instanceManager.GetInstance(Instance);
@@ -141,10 +160,20 @@ namespace Tgstation.Server.Host.Controllers
return Ok();
}
- ///
- #pragma warning disable CA1506 // TODO: Decomplexify
+ ///
+ /// Update watchdog settings to be applied at next server reboot.
+ ///
+ /// The updated settings.
+ /// The for the operation.
+ /// A resulting in the of the operation.
+ /// Settings applied successfully.
+ /// Instance no longer available.
+ [HttpPost]
[TgsAuthorize(DreamDaemonRights.SetAutoStart | DreamDaemonRights.SetPorts | DreamDaemonRights.SetSecurity | DreamDaemonRights.SetWebClient | DreamDaemonRights.SoftRestart | DreamDaemonRights.SoftShutdown | DreamDaemonRights.Start | DreamDaemonRights.SetStartupTimeout)]
- public override async Task Update([FromBody] DreamDaemon model, CancellationToken cancellationToken)
+ [ProducesResponseType(typeof(DreamDaemon), 200)]
+ [ProducesResponseType(410)]
+ #pragma warning disable CA1506 // TODO: Decomplexify
+ public async Task Update([FromBody] DreamDaemon model, CancellationToken cancellationToken)
{
if (model == null)
throw new ArgumentNullException(nameof(model));
@@ -213,12 +242,14 @@ namespace Tgstation.Server.Host.Controllers
#pragma warning restore CA1506
///
- /// Handle a HTTP PATCH to the
+ /// Creates a to restart the Watchdog. It will start if it wasn't already running.
///
/// The for the operation
/// A resulting in the of the request
+ /// Restart started successfully.
[HttpPatch]
[TgsAuthorize(DreamDaemonRights.Restart)]
+ [ProducesResponseType(typeof(Api.Models.Job), 202)]
public async Task Restart(CancellationToken cancellationToken)
{
var job = new Models.Job
diff --git a/src/Tgstation.Server.Host/Controllers/DreamMakerController.cs b/src/Tgstation.Server.Host/Controllers/DreamMakerController.cs
index f9d0a57451..1a9b4e6bcd 100644
--- a/src/Tgstation.Server.Host/Controllers/DreamMakerController.cs
+++ b/src/Tgstation.Server.Host/Controllers/DreamMakerController.cs
@@ -2,6 +2,7 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using System;
+using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Threading;
@@ -17,11 +18,11 @@ using Tgstation.Server.Host.Security;
namespace Tgstation.Server.Host.Controllers
{
///
- /// Controller for managing the compiler
+ /// for managing the deployment system.
///
[Route(Routes.DreamMaker)]
#pragma warning disable CA1506 // TODO: Decomplexify
- public sealed class DreamMakerController : ModelController
+ public sealed class DreamMakerController : ApiController
{
///
/// The for the
@@ -41,24 +42,41 @@ namespace Tgstation.Server.Host.Controllers
/// The value of
/// The value of
/// The for the
- public DreamMakerController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, IJobManager jobManager, IInstanceManager instanceManager, ILogger logger) : base(databaseContext, authenticationContextFactory, logger, true)
+ public DreamMakerController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, IJobManager jobManager, IInstanceManager instanceManager, ILogger logger) : base(databaseContext, authenticationContextFactory, logger, true, true)
{
this.jobManager = jobManager ?? throw new ArgumentNullException(nameof(jobManager));
this.instanceManager = instanceManager ?? throw new ArgumentNullException(nameof(instanceManager));
}
- ///
+ ///
+ /// Read current status.
+ ///
+ /// The for the operation.
+ /// A resulting in the of the request.
+ /// Read status successfully.
+ [HttpGet]
[TgsAuthorize(DreamMakerRights.Read)]
- public override async Task Read(CancellationToken cancellationToken)
+ [ProducesResponseType(typeof(DreamMaker), 200)]
+ public async Task Read(CancellationToken cancellationToken)
{
var instance = instanceManager.GetInstance(Instance);
var dreamMakerSettings = await DatabaseContext.DreamMakerSettings.Where(x => x.InstanceId == Instance.Id).FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false);
return Json(dreamMakerSettings.ToApi());
}
- ///
+ ///
+ /// Get a specified by a given .
+ ///
+ /// The .
+ /// The for the operation.
+ /// A resulting in the of the request.
+ /// retrieved successfully.
+ /// Specified compile job does not exist in this instance.
+ [HttpGet("{id}")]
[TgsAuthorize(DreamMakerRights.CompileJobs)]
- public override async Task GetId(long id, CancellationToken cancellationToken)
+ [ProducesResponseType(typeof(Api.Models.CompileJob), 200)]
+ [ProducesResponseType(404)]
+ public async Task GetId(long id, CancellationToken cancellationToken)
{
var compileJob = await DatabaseContext.CompileJobs
.Where(x => x.Id == id && x.Job.Instance.Id == Instance.Id)
@@ -71,20 +89,34 @@ namespace Tgstation.Server.Host.Controllers
return Json(compileJob.ToApi());
}
- ///
+ ///
+ /// List all s for the instance.
+ ///
+ /// The for the operation.
+ /// A resulting in the of the request.
+ /// Retrieved s successfully.
+ [HttpGet(Routes.List)]
[TgsAuthorize(DreamMakerRights.CompileJobs)]
- public override async Task List(CancellationToken cancellationToken)
+ [ProducesResponseType(typeof(List), 200)]
+ public async Task List(CancellationToken cancellationToken)
{
- var compileJobs = await DatabaseContext.CompileJobs.Where(x => x.Job.Instance.Id == Instance.Id).OrderByDescending(x => x.Job.StoppedAt).Select(x => new Api.Models.CompileJob
+ var compileJobs = await DatabaseContext.CompileJobs.Where(x => x.Job.Instance.Id == Instance.Id).OrderByDescending(x => x.Job.StoppedAt).Select(x => new EntityId
{
Id = x.Id
}).ToListAsync(cancellationToken).ConfigureAwait(false);
return Json(compileJobs);
}
- ///
+ ///
+ /// Begin deploying repository code.
+ ///
+ /// The for the operation.
+ /// A resulting in the of the request.
+ /// Created deployment successfully.
+ [HttpPut]
[TgsAuthorize(DreamMakerRights.Compile)]
- public override async Task Create([FromBody] DreamMaker model, CancellationToken cancellationToken)
+ [ProducesResponseType(typeof(Api.Models.Job), 202)]
+ public async Task Create(CancellationToken cancellationToken)
{
var job = new Models.Job
{
@@ -98,10 +130,24 @@ namespace Tgstation.Server.Host.Controllers
return Accepted(job.ToApi());
}
- ///
+ ///
+ /// Update deployment settings.
+ ///
+ /// The updated settings.
+ /// The for the operation.
+ /// A resulting in the of the request.
+ /// Changes applied successfully. The updated settings will be returned based on user permissions.
+ /// Instance no longer available.
+ [HttpPost]
[TgsAuthorize(DreamMakerRights.SetDme | DreamMakerRights.SetApiValidationPort | DreamMakerRights.SetApiValidationPort)]
- public override async Task Update([FromBody] DreamMaker model, CancellationToken cancellationToken)
+ [ProducesResponseType(typeof(DreamMaker), 200)]
+ [ProducesResponseType(200)]
+ [ProducesResponseType(410)]
+ public async Task Update([FromBody] DreamMaker model, CancellationToken cancellationToken)
{
+ if (model == null)
+ throw new ArgumentNullException(nameof(model));
+
if (model.ApiValidationPort == 0)
return BadRequest(new ErrorMessage { Message = "API Validation port cannot be 0!" });
diff --git a/src/Tgstation.Server.Host/Controllers/HomeController.cs b/src/Tgstation.Server.Host/Controllers/HomeController.cs
index baea142ac0..f278244ddc 100644
--- a/src/Tgstation.Server.Host/Controllers/HomeController.cs
+++ b/src/Tgstation.Server.Host/Controllers/HomeController.cs
@@ -1,8 +1,11 @@
using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
+using Microsoft.Extensions.Primitives;
+using Microsoft.Net.Http.Headers;
using System;
using System.Linq;
using System.Net.Mime;
@@ -85,10 +88,14 @@ namespace Tgstation.Server.Host.Controllers
///
/// Main page of the
///
- /// The of the if a properly authenticated API request, the web control panel if on a browser and enabled, otherwise
+ ///
+ /// The of the if a properly authenticated API request, the web control panel if on a browser and enabled, otherwise.
+ ///
+ /// retrieved successfully.
+ [HttpGet]
[TgsAuthorize]
[AllowAnonymous]
- [HttpGet]
+ [ProducesResponseType(typeof(Api.Models.ServerInformation), 200)]
public IActionResult Home()
{
if (AuthenticationContext != null)
@@ -113,12 +120,33 @@ namespace Tgstation.Server.Host.Controllers
///
/// The for the operation
/// A resulting in the of the operation
+ /// User logged in and generated successfully.
+ /// User authentication failed.
+ /// User authenticated but is disabled by an administrator.
[HttpPost]
+ [ProducesResponseType(typeof(Api.Models.Token), 200)]
+ [ProducesResponseType(401)]
+ [ProducesResponseType(403)]
#pragma warning disable CA1506 // TODO: Decomplexify
public async Task CreateToken(CancellationToken cancellationToken)
{
if (ApiHeaders == null)
- return BadRequest(new Api.Models.ErrorMessage { Message = "Missing API headers!" });
+ {
+ // Get the exact error
+ var errorMessage = "Missing API headers!";
+ try
+ {
+ var _ = new ApiHeaders(Request.GetTypedHeaders());
+ }
+ catch (InvalidOperationException ex)
+ {
+ errorMessage = ex.Message;
+ }
+
+ Response.Headers.Add(HeaderNames.WWWAuthenticate, new StringValues("basic realm=\"Create TGS4 bearer token\""));
+
+ return BadRequest(new Api.Models.ErrorMessage { Message = errorMessage });
+ }
if (ApiHeaders.IsTokenAuthentication)
return BadRequest(new Api.Models.ErrorMessage { Message = "Cannot create a token using another token!" });
@@ -197,7 +225,10 @@ namespace Tgstation.Server.Host.Controllers
// Now that the bookeeping is done, tell them to fuck off if necessary
if (!user.Enabled.Value)
+ {
+ Logger.LogTrace("Not logging in disabled user {0}.", user.Id);
return Forbid();
+ }
var token = await tokenFactory.CreateToken(user, cancellationToken).ConfigureAwait(false);
if (usingSystemIdentity)
diff --git a/src/Tgstation.Server.Host/Controllers/InstanceController.cs b/src/Tgstation.Server.Host/Controllers/InstanceController.cs
index 22419b3be2..1481b1019f 100644
--- a/src/Tgstation.Server.Host/Controllers/InstanceController.cs
+++ b/src/Tgstation.Server.Host/Controllers/InstanceController.cs
@@ -23,11 +23,11 @@ using Tgstation.Server.Host.Security;
namespace Tgstation.Server.Host.Controllers
{
///
- /// Controller for managing s
+ /// for managing s
///
[Route(Routes.InstanceManager)]
#pragma warning disable CA1506 // TODO: Decomplexify
- public sealed class InstanceController : ModelController
+ public sealed class InstanceController : ApiController
{
///
/// File name to allow attaching instances
@@ -72,7 +72,7 @@ namespace Tgstation.Server.Host.Controllers
/// The value of
/// The value of
/// The for the
- public InstanceController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, IJobManager jobManager, IInstanceManager instanceManager, IIOManager ioManager, IApplication application, IPlatformIdentifier platformIdentifier, ILogger logger) : base(databaseContext, authenticationContextFactory, logger, false)
+ public InstanceController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, IJobManager jobManager, IInstanceManager instanceManager, IIOManager ioManager, IApplication application, IPlatformIdentifier platformIdentifier, ILogger logger) : base(databaseContext, authenticationContextFactory, logger, false, true)
{
this.jobManager = jobManager ?? throw new ArgumentNullException(nameof(jobManager));
this.instanceManager = instanceManager ?? throw new ArgumentNullException(nameof(instanceManager));
@@ -108,9 +108,19 @@ namespace Tgstation.Server.Host.Controllers
UserId = AuthenticationContext.User.Id
};
- ///
+ ///
+ /// Create or attach an .
+ ///
+ /// The settings.
+ /// The for the operation.
+ /// A resulting in the of the request.
+ /// Instance attached successfully.
+ /// Instance created successfully.
+ [HttpPut]
[TgsAuthorize(InstanceManagerRights.Create)]
- public override async Task Create([FromBody] Api.Models.Instance model, CancellationToken cancellationToken)
+ [ProducesResponseType(typeof(Api.Models.Instance), 200)]
+ [ProducesResponseType(typeof(Api.Models.Instance), 201)]
+ public async Task Create([FromBody] Api.Models.Instance model, CancellationToken cancellationToken)
{
if (model == null)
throw new ArgumentNullException(nameof(model));
@@ -130,7 +140,14 @@ namespace Tgstation.Server.Host.Controllers
}, out var normalizedLocalPath);
if (rawPath.StartsWith(normalizedLocalPath, StringComparison.Ordinal))
- return Conflict("Instances cannot be created in the installation directory!");
+ {
+ bool sameLength = rawPath.Length == normalizedLocalPath.Length;
+ char dirSeparatorChar = rawPath.ToCharArray()[normalizedLocalPath.Length];
+ if(sameLength
+ || dirSeparatorChar == Path.DirectorySeparatorChar
+ || dirSeparatorChar == Path.AltDirectorySeparatorChar)
+ return Conflict(new ErrorMessage { Message = "Instances cannot be created in the installation directory!" });
+ }
var dirExistsTask = ioManager.DirectoryExists(model.Path, cancellationToken);
bool attached = false;
@@ -214,9 +231,19 @@ namespace Tgstation.Server.Host.Controllers
return attached ? (IActionResult)Json(api) : StatusCode((int)HttpStatusCode.Created, api);
}
- ///
+ ///
+ /// Detach an with the given .
+ ///
+ /// The to detach.
+ /// The for the operation.
+ /// A resulting in the of the request.
+ /// Instance detatched successfully.
+ /// Instance not available.
+ [HttpDelete("{id}")]
[TgsAuthorize(InstanceManagerRights.Delete)]
- public override async Task Delete(long id, CancellationToken cancellationToken)
+ [ProducesResponseType(200)]
+ [ProducesResponseType(410)]
+ public async Task Delete(long id, CancellationToken cancellationToken)
{
var originalModel = await DatabaseContext.Instances.Where(x => x.Id == id)
.Include(x => x.WatchdogReattachInformation)
@@ -248,11 +275,24 @@ namespace Tgstation.Server.Host.Controllers
return Ok();
}
- ///
+ ///
+ /// Modify an 's settings.
+ ///
+ /// The updated settings.
+ /// The for the operation.
+ /// A resulting in the of the request.
+ /// Instance updated successfully.
+ /// Instance updated successfully and relocation job created.
+ [HttpPost]
[TgsAuthorize(InstanceManagerRights.Relocate | InstanceManagerRights.Rename | InstanceManagerRights.SetAutoUpdate | InstanceManagerRights.SetConfiguration | InstanceManagerRights.SetOnline)]
- #pragma warning disable CA1502 // TODO: Decomplexify
- public override async Task Update([FromBody] Api.Models.Instance model, CancellationToken cancellationToken)
+ [ProducesResponseType(typeof(Api.Models.Instance), 200)]
+ [ProducesResponseType(410)]
+#pragma warning disable CA1502 // TODO: Decomplexify
+ public async Task Update([FromBody] Api.Models.Instance model, CancellationToken cancellationToken)
{
+ if (model == null)
+ throw new ArgumentNullException(nameof(model));
+
var instanceQuery = DatabaseContext.Instances.Where(x => x.Id == model.Id);
var moveJob = await instanceQuery
@@ -372,7 +412,9 @@ namespace Tgstation.Server.Host.Controllers
{
Id = originalModel.Id
};
- if (originalModelPath != null)
+
+ var moving = originalModelPath != null;
+ if (moving)
{
var job = new Models.Job
{
@@ -390,13 +432,20 @@ namespace Tgstation.Server.Host.Controllers
if (originalModel.Online.Value && model.AutoUpdateInterval.HasValue && oldAutoUpdateInterval != model.AutoUpdateInterval)
await instanceManager.GetInstance(originalModel).SetAutoUpdateInterval(model.AutoUpdateInterval.Value).ConfigureAwait(false);
- return Json(api);
+ return moving ? (IActionResult)Accepted(api) : Json(api);
}
- #pragma warning restore CA1502
+#pragma warning restore CA1502
- ///
+ ///
+ /// List s.
+ ///
+ /// The for the operation.
+ /// A resulting in the of the request.
+ /// Retrieved s successfully.
+ [HttpGet(Routes.List)]
[TgsAuthorize(InstanceManagerRights.List | InstanceManagerRights.Read)]
- public override async Task List(CancellationToken cancellationToken)
+ [ProducesResponseType(typeof(IEnumerable), 200)]
+ public async Task List(CancellationToken cancellationToken)
{
IQueryable query = DatabaseContext.Instances;
if (!AuthenticationContext.User.InstanceManagerRights.Value.HasFlag(InstanceManagerRights.List))
@@ -418,9 +467,19 @@ namespace Tgstation.Server.Host.Controllers
return Json(apis);
}
- ///
+ ///
+ /// Get a specific .
+ ///
+ /// The to retrieve.
+ /// The for the operation.
+ /// A resulting in the of the request.
+ /// Retrieved successfully.
+ /// Instance not available.
+ [HttpGet("{id}")]
[TgsAuthorize(InstanceManagerRights.List | InstanceManagerRights.Read)]
- public override async Task GetId(long id, CancellationToken cancellationToken)
+ [ProducesResponseType(typeof(Api.Models.Instance), 200)]
+ [ProducesResponseType(410)]
+ public async Task GetId(long id, CancellationToken cancellationToken)
{
var query = DatabaseContext.Instances.Where(x => x.Id == id);
var cantList = !AuthenticationContext.User.InstanceManagerRights.Value.HasFlag(InstanceManagerRights.List);
diff --git a/src/Tgstation.Server.Host/Controllers/InstanceUserController.cs b/src/Tgstation.Server.Host/Controllers/InstanceUserController.cs
index 67a4d0189d..dc998b9788 100644
--- a/src/Tgstation.Server.Host/Controllers/InstanceUserController.cs
+++ b/src/Tgstation.Server.Host/Controllers/InstanceUserController.cs
@@ -2,6 +2,7 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using System;
+using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Threading;
@@ -16,10 +17,10 @@ using Z.EntityFramework.Plus;
namespace Tgstation.Server.Host.Controllers
{
///
- /// For managing s
+ /// for managing s.
///
[Route(Routes.InstanceUser)]
- public sealed class InstanceUserController : ModelController
+ public sealed class InstanceUserController : ApiController
{
///
/// Construct a
@@ -27,7 +28,7 @@ namespace Tgstation.Server.Host.Controllers
/// The for the
/// The for the
/// The for the
- public InstanceUserController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, ILogger logger) : base(databaseContext, authenticationContextFactory, logger, true) // false instance requirement, we handle this ourself
+ public InstanceUserController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, ILogger logger) : base(databaseContext, authenticationContextFactory, logger, true, true)
{ }
///
@@ -46,9 +47,17 @@ namespace Tgstation.Server.Host.Controllers
return null;
}
- ///
+ ///
+ /// Create am .
+ ///
+ /// The to create.
+ /// The for the operation.
+ /// A resulting in the of the request.
+ /// created successfully.
+ [HttpPut]
[TgsAuthorize(InstanceUserRights.CreateUsers)]
- public override async Task Create([FromBody] Api.Models.InstanceUser model, CancellationToken cancellationToken)
+ [ProducesResponseType(typeof(Api.Models.InstanceUser), 201)]
+ public async Task Create([FromBody] Api.Models.InstanceUser model, CancellationToken cancellationToken)
{
var test = StandardModelChecks(model);
if (test != null)
@@ -73,10 +82,20 @@ namespace Tgstation.Server.Host.Controllers
return StatusCode((int)HttpStatusCode.Created, dbUser.ToApi());
}
- ///
+ ///
+ /// Update the permissions for an .
+ ///
+ /// The updated .
+ /// The for the operation.
+ /// A resulting in the of the request.
+ /// updated successfully.
+ /// Instance user unavailable.
+ [HttpPost]
[TgsAuthorize(InstanceUserRights.WriteUsers)]
+ [ProducesResponseType(typeof(Api.Models.InstanceUser), 200)]
+ [ProducesResponseType(410)]
#pragma warning disable CA1506 // TODO: Decomplexify
- public override async Task Update([FromBody] Api.Models.InstanceUser model, CancellationToken cancellationToken)
+ public async Task Update([FromBody] Api.Models.InstanceUser model, CancellationToken cancellationToken)
{
var test = StandardModelChecks(model);
if (test != null)
@@ -100,23 +119,45 @@ namespace Tgstation.Server.Host.Controllers
UserId = originalUser.UserId
});
}
- #pragma warning restore CA1506
-
- ///
+#pragma warning restore CA1506
+ ///
+ /// Read the active .
+ ///
+ /// The of the request.
+ /// retrieved successfully.
+ [HttpGet]
[TgsAuthorize]
- public override Task Read(CancellationToken cancellationToken) => Task.FromResult(AuthenticationContext.InstanceUser != null ? (IActionResult)Json(AuthenticationContext.InstanceUser.ToApi()) : NotFound());
+ [ProducesResponseType(typeof(Api.Models.InstanceUser), 200)]
+ public IActionResult Read() => Json(AuthenticationContext.InstanceUser.ToApi());
- ///
+ ///
+ /// Lists s for the instance.
+ ///
+ /// The for the operation.
+ /// A resulting in the of the request.
+ /// Retrieved s successfully.
+ [HttpGet(Routes.List)]
[TgsAuthorize(InstanceUserRights.ReadUsers)]
- public override async Task List(CancellationToken cancellationToken)
+ [ProducesResponseType(typeof(IEnumerable), 200)]
+ public async Task List(CancellationToken cancellationToken)
{
var users = await DatabaseContext.Instances.Where(x => x.Id == Instance.Id).SelectMany(x => x.InstanceUsers).ToListAsync(cancellationToken).ConfigureAwait(false);
return Json(users.Select(x => x.ToApi()));
}
- ///
+ ///
+ /// Gets a specific .
+ ///
+ /// The .
+ /// The for the operation.
+ /// A resulting in the of the request.
+ /// Retrieve successfully.
+ /// Instance user unavailable.
+ [HttpGet("{id}")]
[TgsAuthorize(InstanceUserRights.ReadUsers)]
- public override async Task GetId(long id, CancellationToken cancellationToken)
+ [ProducesResponseType(typeof(Api.Models.InstanceUser), 200)]
+ [ProducesResponseType(410)]
+ public async Task GetId(long id, CancellationToken cancellationToken)
{
// this functions as userId
var user = await DatabaseContext.Instances.Where(x => x.Id == Instance.Id).SelectMany(x => x.InstanceUsers).Where(x => x.UserId == id).FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false);
@@ -125,9 +166,17 @@ namespace Tgstation.Server.Host.Controllers
return Json(user.ToApi());
}
- ///
+ ///
+ /// Delete an .
+ ///
+ /// The to delete.
+ /// The for the operation.
+ /// A resulting in the of the request.
+ /// deleted or no longer exists.
+ [HttpDelete("{id}")]
[TgsAuthorize(InstanceUserRights.WriteUsers)]
- public override async Task Delete(long id, CancellationToken cancellationToken)
+ [ProducesResponseType(200)]
+ public async Task Delete(long id, CancellationToken cancellationToken)
{
await DatabaseContext.Instances.Where(x => x.Id == Instance.Id).SelectMany(x => x.InstanceUsers).Where(x => x.UserId == id).DeleteAsync(cancellationToken).ConfigureAwait(false);
return Ok();
diff --git a/src/Tgstation.Server.Host/Controllers/JobController.cs b/src/Tgstation.Server.Host/Controllers/JobController.cs
index e5c08bbc99..5dddb4bd01 100644
--- a/src/Tgstation.Server.Host/Controllers/JobController.cs
+++ b/src/Tgstation.Server.Host/Controllers/JobController.cs
@@ -2,6 +2,7 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using System;
+using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Threading;
@@ -14,10 +15,10 @@ using Tgstation.Server.Host.Security;
namespace Tgstation.Server.Host.Controllers
{
///
- /// for s
+ /// for s
///
[Route(Routes.Jobs)]
- public sealed class JobController : ModelController
+ public sealed class JobController : ApiController
{
///
/// The for the
@@ -31,37 +32,63 @@ namespace Tgstation.Server.Host.Controllers
/// The for the
/// The value of
/// The for the
- public JobController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, IJobManager jobManager, ILogger logger) : base(databaseContext, authenticationContextFactory, logger, true)
+ public JobController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, IJobManager jobManager, ILogger logger) : base(databaseContext, authenticationContextFactory, logger, true, true)
{
this.jobManager = jobManager ?? throw new ArgumentNullException(nameof(jobManager));
}
- ///
+ ///
+ /// Get active s for the instance.
+ ///
+ /// The for the operation.
+ /// A resulting in the of the request.
+ /// Retrieved active s successfully.
+ [HttpGet]
[TgsAuthorize]
- public override async Task Read(CancellationToken cancellationToken)
+ [ProducesResponseType(typeof(IEnumerable), 200)]
+ public async Task Read(CancellationToken cancellationToken)
{
var result = await DatabaseContext.Jobs.Where(x => x.Instance.Id == Instance.Id && !x.StoppedAt.HasValue).OrderByDescending(x => x.StartedAt).ToListAsync(cancellationToken).ConfigureAwait(false);
return Json(result.Select(x => x.ToApi()));
}
- ///
+ ///
+ /// List all s for the instance in reverse creation order.
+ ///
+ /// The for the operation.
+ /// A resulting in the of the request.
+ /// Retrieved s successfully.
+ [HttpGet(Routes.List)]
[TgsAuthorize]
- public override async Task List(CancellationToken cancellationToken)
+ [ProducesResponseType(typeof(List), 200)]
+ public async Task List(CancellationToken cancellationToken)
{
// you KNOW this will need pagination eventually right?
- var jobs = await DatabaseContext.Jobs.Where(x => x.Instance.Id == Instance.Id).OrderByDescending(x => x.StartedAt).Select(x => new Api.Models.Job
+ var jobs = await DatabaseContext.Jobs.Where(x => x.Instance.Id == Instance.Id).OrderByDescending(x => x.StartedAt).Select(x => new Api.Models.EntityId
{
Id = x.Id
}).ToListAsync(cancellationToken).ConfigureAwait(false);
return Json(jobs);
}
- ///
+ ///
+ /// Cancel a running .
+ ///
+ /// The of the to cancel.
+ /// The for the operation.
+ /// A resulting in the of the request.
+ /// cancellation requested successfully.
+ /// does not exist in this instance.
+ /// already cancelled or completed.
+ [HttpDelete("{id}")]
[TgsAuthorize]
- public override async Task Delete(long id, CancellationToken cancellationToken)
+ [ProducesResponseType(202)]
+ [ProducesResponseType(404)]
+ [ProducesResponseType(410)]
+ public async Task Delete(long id, CancellationToken cancellationToken)
{
// don't care if an instance post or not at this point
- var job = await DatabaseContext.Jobs.Where(x => x.Id == id).FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false);
+ var job = await DatabaseContext.Jobs.Where(x => x.Id == id && x.Instance.Id == Instance.Id).FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false);
if (job == default(Job))
return NotFound();
@@ -75,11 +102,21 @@ namespace Tgstation.Server.Host.Controllers
return cancelled ? (IActionResult)Accepted() : StatusCode((int)HttpStatusCode.Gone);
}
- ///
+ ///
+ /// Get a specific .
+ ///
+ /// The of the to retrieve.
+ /// The for the operation.
+ /// A resulting in the of the request.
+ /// Retrieved successfully.
+ /// does not exist in this instance.
+ [HttpGet("{id}")]
[TgsAuthorize]
- public override async Task GetId(long id, CancellationToken cancellationToken)
+ [ProducesResponseType(typeof(Api.Models.Job), 200)]
+ [ProducesResponseType(404)]
+ public async Task GetId(long id, CancellationToken cancellationToken)
{
- var job = await DatabaseContext.Jobs.Where(x => x.Id == id).Include(x => x.StartedBy).FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false);
+ var job = await DatabaseContext.Jobs.Where(x => x.Id == id && x.Instance.Id == Instance.Id).Include(x => x.StartedBy).FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false);
if (job == default(Job))
return NotFound();
var api = job.ToApi();
diff --git a/src/Tgstation.Server.Host/Controllers/ModelController.cs b/src/Tgstation.Server.Host/Controllers/ModelController.cs
deleted file mode 100644
index 3434992040..0000000000
--- a/src/Tgstation.Server.Host/Controllers/ModelController.cs
+++ /dev/null
@@ -1,77 +0,0 @@
-using Microsoft.AspNetCore.Mvc;
-using Microsoft.Extensions.Logging;
-using System.Threading;
-using System.Threading.Tasks;
-using Tgstation.Server.Host.Models;
-using Tgstation.Server.Host.Security;
-
-namespace Tgstation.Server.Host.Controllers
-{
- ///
- /// An representing a
- ///
- /// The model being represented
- public abstract class ModelController : ApiController where TModel : class
- {
- ///
- /// Construct a
- ///
- /// The for the
- /// The for the
- /// The for the
- /// If the requires an
- public ModelController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, ILogger logger, bool requireInstance) : base(databaseContext, authenticationContextFactory, logger, requireInstance, true) { }
-
- ///
- /// Attempt to create a
- ///
- /// The being created
- /// The for the operation
- /// A resulting in the of the operation
- [HttpPut]
- public virtual Task Create([FromBody]TModel model, CancellationToken cancellationToken) => Task.FromResult((IActionResult)NotFound());
-
- ///
- /// Attempt to read a
- ///
- /// The for the operation
- /// A resulting in the of the operation
- [HttpGet]
- public virtual Task Read(CancellationToken cancellationToken) => Task.FromResult((IActionResult)NotFound());
-
- ///
- /// Attempt to get a specific a
- ///
- /// The ID of the model to get
- /// The for the operation
- /// A resulting in the of the operation
- [HttpGet("{id}")]
- public virtual Task GetId(long id, CancellationToken cancellationToken) => Task.FromResult((IActionResult)NotFound());
-
- ///
- /// Attempt to update a
- ///
- /// The being updated
- /// The for the operation
- /// A resulting in the of the operation
- [HttpPost]
- public virtual Task Update([FromBody]TModel model, CancellationToken cancellationToken) => Task.FromResult((IActionResult)NotFound());
-
- ///
- /// Attempt to delete a model with a particular
- ///
- /// The ID of the model to delete
- /// The for the operation
- /// A resulting in the of the operation
- [HttpDelete("{id}")]
- public virtual Task Delete(long id, CancellationToken cancellationToken) => Task.FromResult((IActionResult)NotFound());
-
- ///
- /// Attempt to list entries of the
- ///
- /// The for the operation
- /// A resulting in the of the operation
- [HttpGet("List")]
- public virtual Task List(CancellationToken cancellationToken) => Task.FromResult((IActionResult)NotFound());
- }
-}
diff --git a/src/Tgstation.Server.Host/Controllers/RepositoryController.cs b/src/Tgstation.Server.Host/Controllers/RepositoryController.cs
index 69d0d6417b..c6fd18a741 100644
--- a/src/Tgstation.Server.Host/Controllers/RepositoryController.cs
+++ b/src/Tgstation.Server.Host/Controllers/RepositoryController.cs
@@ -23,11 +23,11 @@ using Tgstation.Server.Host.Security;
namespace Tgstation.Server.Host.Controllers
{
///
- /// Controller for managing the s
+ /// for managing the s
///
[Route(Routes.Repository)]
#pragma warning disable CA1506 // TODO: Decomplexify
- public sealed class RepositoryController : ModelController
+ public sealed class RepositoryController : ApiController
{
///
/// The for the
@@ -59,7 +59,7 @@ namespace Tgstation.Server.Host.Controllers
/// The value of
/// The for the
/// The containing value of
- public RepositoryController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, IInstanceManager instanceManager, IGitHubClientFactory gitHubClientFactory, IJobManager jobManager, ILogger logger, IOptions generalConfigurationOptions) : base(databaseContext, authenticationContextFactory, logger, true)
+ public RepositoryController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, IInstanceManager instanceManager, IGitHubClientFactory gitHubClientFactory, IJobManager jobManager, ILogger logger, IOptions generalConfigurationOptions) : base(databaseContext, authenticationContextFactory, logger, true, true)
{
this.instanceManager = instanceManager ?? throw new ArgumentNullException(nameof(instanceManager));
this.gitHubClientFactory = gitHubClientFactory ?? throw new ArgumentNullException(nameof(gitHubClientFactory));
@@ -126,9 +126,19 @@ namespace Tgstation.Server.Host.Controllers
return needsDbUpdate;
}
- ///
+ ///
+ /// Begin cloning the repository if it doesn't exist.
+ ///
+ /// Initial settings.
+ /// The for the operation.
+ /// A resulting in the of the request.
+ /// The was created successfully and the to clone it has begun.
+ /// Instance no longer available.
+ [HttpPut]
[TgsAuthorize(RepositoryRights.SetOrigin)]
- public override async Task Create([FromBody] Repository model, CancellationToken cancellationToken)
+ [ProducesResponseType(typeof(Repository), 201)]
+ [ProducesResponseType(410)]
+ public async Task Create([FromBody] Repository model, CancellationToken cancellationToken)
{
if (model == null)
throw new ArgumentNullException(nameof(model));
@@ -214,11 +224,16 @@ namespace Tgstation.Server.Host.Controllers
}
///
- /// Delete the
+ /// Delete the .
///
/// The for the operation
/// A resulting in the of the operation
+ /// Job to delete the repository created successfully.
+ /// Instance no longer available.
+ [HttpDelete("{id}")]
[TgsAuthorize(RepositoryRights.Delete)]
+ [ProducesResponseType(typeof(Repository), 202)]
+ [ProducesResponseType(410)]
public async Task Delete(CancellationToken cancellationToken)
{
var currentModel = await DatabaseContext.RepositorySettings.Where(x => x.InstanceId == Instance.Id).FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false);
@@ -245,9 +260,20 @@ namespace Tgstation.Server.Host.Controllers
return Accepted(api);
}
- ///
+ ///
+ /// Get status.
+ ///
+ /// The for the operation.
+ /// A resulting in the of the operation.
+ /// Retrieved the settings successfully.
+ /// Retrieved the settings successfully, though they did not previously exist.
+ /// Instance no longer available.
+ [HttpGet]
[TgsAuthorize(RepositoryRights.Read)]
- public override async Task Read(CancellationToken cancellationToken)
+ [ProducesResponseType(typeof(Repository), 200)]
+ [ProducesResponseType(typeof(Repository), 201)]
+ [ProducesResponseType(410)]
+ public async Task Read(CancellationToken cancellationToken)
{
var currentModel = await DatabaseContext.RepositorySettings.Where(x => x.InstanceId == Instance.Id).FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false);
@@ -273,7 +299,7 @@ namespace Tgstation.Server.Host.Controllers
{
if (repo != null && await PopulateApi(api, repo, DatabaseContext, Instance, cancellationToken).ConfigureAwait(false))
{
- // user may have fucked with the repo without telling us, do what we can
+ // user may have fucked with the repo manually, do what we can
await DatabaseContext.Save(cancellationToken).ConfigureAwait(false);
return StatusCode((int)HttpStatusCode.Created, api);
}
@@ -282,11 +308,23 @@ namespace Tgstation.Server.Host.Controllers
}
}
- ///
+ ///
+ /// Perform updats to the .
+ ///
+ /// The updated .
+ /// The for the operation.
+ /// A resulting in the of the operation.
+ /// Updated the settings successfully.
+ /// Updated the settings successfully and a was created to make the requested git changes.
+ /// Instance no longer available.
+ [HttpPost]
[TgsAuthorize(RepositoryRights.ChangeAutoUpdateSettings | RepositoryRights.ChangeCommitter | RepositoryRights.ChangeCredentials | RepositoryRights.ChangeTestMergeCommits | RepositoryRights.MergePullRequest | RepositoryRights.SetReference | RepositoryRights.SetSha | RepositoryRights.UpdateBranch)]
+ [ProducesResponseType(typeof(Repository), 200)]
+ [ProducesResponseType(typeof(Repository), 202)]
+ [ProducesResponseType(410)]
#pragma warning disable CA1502 // TODO: Decomplexify
#pragma warning disable CA1505
- public override async Task Update([FromBody]Repository model, CancellationToken cancellationToken)
+ public async Task Update([FromBody]Repository model, CancellationToken cancellationToken)
{
if (model == null)
throw new ArgumentNullException(nameof(model));
diff --git a/src/Tgstation.Server.Host/Controllers/TgsAuthorizeAttribute.cs b/src/Tgstation.Server.Host/Controllers/TgsAuthorizeAttribute.cs
index 0d52ac6ae1..4501a9783f 100644
--- a/src/Tgstation.Server.Host/Controllers/TgsAuthorizeAttribute.cs
+++ b/src/Tgstation.Server.Host/Controllers/TgsAuthorizeAttribute.cs
@@ -11,6 +11,11 @@ namespace Tgstation.Server.Host.Controllers
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = true)]
sealed class TgsAuthorizeAttribute : AuthorizeAttribute
{
+ ///
+ /// Gets the associated with the if any.
+ ///
+ public RightsType? RightsType { get; }
+
///
/// Construct a
///
@@ -23,6 +28,7 @@ namespace Tgstation.Server.Host.Controllers
public TgsAuthorizeAttribute(AdministrationRights requiredRights)
{
Roles = RightsHelper.RoleNames(requiredRights);
+ RightsType = Api.Rights.RightsType.Administration;
}
///
@@ -32,6 +38,7 @@ namespace Tgstation.Server.Host.Controllers
public TgsAuthorizeAttribute(InstanceManagerRights requiredRights)
{
Roles = RightsHelper.RoleNames(requiredRights);
+ RightsType = Api.Rights.RightsType.InstanceManager;
}
///
@@ -41,6 +48,7 @@ namespace Tgstation.Server.Host.Controllers
public TgsAuthorizeAttribute(RepositoryRights requiredRights)
{
Roles = RightsHelper.RoleNames(requiredRights);
+ RightsType = Api.Rights.RightsType.Repository;
}
///
@@ -50,6 +58,7 @@ namespace Tgstation.Server.Host.Controllers
public TgsAuthorizeAttribute(ByondRights requiredRights)
{
Roles = RightsHelper.RoleNames(requiredRights);
+ RightsType = Api.Rights.RightsType.Byond;
}
///
@@ -59,6 +68,7 @@ namespace Tgstation.Server.Host.Controllers
public TgsAuthorizeAttribute(DreamMakerRights requiredRights)
{
Roles = RightsHelper.RoleNames(requiredRights);
+ RightsType = Api.Rights.RightsType.DreamMaker;
}
///
@@ -68,6 +78,7 @@ namespace Tgstation.Server.Host.Controllers
public TgsAuthorizeAttribute(DreamDaemonRights requiredRights)
{
Roles = RightsHelper.RoleNames(requiredRights);
+ RightsType = Api.Rights.RightsType.DreamDaemon;
}
///
@@ -77,6 +88,7 @@ namespace Tgstation.Server.Host.Controllers
public TgsAuthorizeAttribute(ChatBotRights requiredRights)
{
Roles = RightsHelper.RoleNames(requiredRights);
+ RightsType = Api.Rights.RightsType.ChatBots;
}
///
@@ -86,6 +98,7 @@ namespace Tgstation.Server.Host.Controllers
public TgsAuthorizeAttribute(ConfigurationRights requiredRights)
{
Roles = RightsHelper.RoleNames(requiredRights);
+ RightsType = Api.Rights.RightsType.Configuration;
}
///
@@ -95,6 +108,7 @@ namespace Tgstation.Server.Host.Controllers
public TgsAuthorizeAttribute(InstanceUserRights requiredRights)
{
Roles = RightsHelper.RoleNames(requiredRights);
+ RightsType = Api.Rights.RightsType.InstanceUser;
}
}
}
diff --git a/src/Tgstation.Server.Host/Controllers/TgsOpenApiFilters.cs b/src/Tgstation.Server.Host/Controllers/TgsOpenApiFilters.cs
new file mode 100644
index 0000000000..b902770af5
--- /dev/null
+++ b/src/Tgstation.Server.Host/Controllers/TgsOpenApiFilters.cs
@@ -0,0 +1,227 @@
+using Microsoft.Net.Http.Headers;
+using Microsoft.OpenApi.Any;
+using Microsoft.OpenApi.Models;
+using Swashbuckle.AspNetCore.SwaggerGen;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Net;
+using Tgstation.Server.Api;
+using Tgstation.Server.Api.Models;
+using Tgstation.Server.Api.Rights;
+
+namespace Tgstation.Server.Host.Controllers
+{
+ ///
+ /// and for the server.
+ ///
+ sealed class TgsOpenApiFilters : IOperationFilter, IDocumentFilter
+ {
+ ///
+ /// The name for password authentication.
+ ///
+ public const string PasswordSecuritySchemeId = "Password_Login_Scheme";
+
+ ///
+ /// The name for token authentication.
+ ///
+ public const string TokenSecuritySchemeId = "Token_Authorization_Scheme";
+
+ ///
+ public void Apply(OpenApiOperation operation, OperationFilterContext context)
+ {
+ if (operation == null)
+ throw new ArgumentNullException(nameof(operation));
+ if (context == null)
+ throw new ArgumentNullException(nameof(context));
+
+ var authAttributes = context
+ .MethodInfo
+ .DeclaringType
+ .GetCustomAttributes(true)
+ .Union(
+ context
+ .MethodInfo
+ .GetCustomAttributes(true))
+ .OfType();
+
+ if (authAttributes.Any())
+ {
+ var tokenScheme = new OpenApiSecurityScheme
+ {
+ Reference = new OpenApiReference
+ {
+ Type = ReferenceType.SecurityScheme,
+ Id = TokenSecuritySchemeId
+ }
+ };
+
+ operation.Security = new List
+ {
+ new OpenApiSecurityRequirement
+ {
+ {
+ tokenScheme,
+ new List()
+ }
+ }
+ };
+
+ if (authAttributes.Any(attr => attr.RightsType.HasValue && RightsHelper.IsInstanceRight(attr.RightsType.Value)))
+ operation.Parameters.Add(new OpenApiParameter
+ {
+ Reference = new OpenApiReference
+ {
+ Type = ReferenceType.Header,
+ Id = ApiHeaders.InstanceIdHeader
+ }
+ });
+ }
+ else
+ {
+ // HomeController.CreateToken
+ var passwordScheme = new OpenApiSecurityScheme
+ {
+ Reference = new OpenApiReference
+ {
+ Type = ReferenceType.SecurityScheme,
+ Id = PasswordSecuritySchemeId
+ }
+ };
+
+ operation.Security = new List
+ {
+ new OpenApiSecurityRequirement
+ {
+ {
+ passwordScheme,
+ new List()
+ }
+ }
+ };
+ }
+ }
+
+ ///
+ public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context)
+ {
+ swaggerDoc.Components.Headers.Add(ApiHeaders.InstanceIdHeader, new OpenApiHeader
+ {
+ Description = "The instance ID being accessed",
+ Required = true,
+ Style = ParameterStyle.Simple
+ });
+
+ swaggerDoc.Components.Headers.Add(ApiHeaders.ApiVersionHeader, new OpenApiHeader
+ {
+ Description = "The API version being used in the form \"Tgstation.Server.Api/[API version]\"",
+ Required = true,
+ Style = ParameterStyle.Simple,
+ Example = new OpenApiString($"Tgstation.Server.Api/{ApiHeaders.Version}")
+ });
+
+ swaggerDoc.Components.Headers.Add(HeaderNames.UserAgent, new OpenApiHeader
+ {
+ Description = "The user agent of the calling client.",
+ Required = true,
+ Style = ParameterStyle.Simple,
+ Example = new OpenApiString("Your-user-agent/1.0.0.0")
+ });
+
+ foreach (var operation in swaggerDoc
+ .Paths
+ .SelectMany(path => path.Value.Operations)
+ .Select(kvp => kvp.Value))
+ {
+ operation.Parameters.Add(new OpenApiParameter
+ {
+ Reference = new OpenApiReference
+ {
+ Type = ReferenceType.Header,
+ Id = ApiHeaders.ApiVersionHeader
+ }
+ });
+
+ operation.Parameters.Add(new OpenApiParameter
+ {
+ Reference = new OpenApiReference
+ {
+ Type = ReferenceType.Header,
+ Id = HeaderNames.UserAgent
+ }
+ });
+ }
+
+ var errorMessageContent = new Dictionary
+ {
+ {
+ ApiHeaders.ApplicationJson,
+ new OpenApiMediaType
+ {
+ Schema = new OpenApiSchema
+ {
+ Reference = new OpenApiReference
+ {
+ Id = nameof(ErrorMessage),
+ Type = ReferenceType.Schema
+ }
+ }
+ }
+ }
+ };
+
+ void AddDefaultResponse(HttpStatusCode code, OpenApiResponse concrete)
+ {
+ string responseKey = $"{(int)code}";
+
+ swaggerDoc.Components.Responses.Add(responseKey, concrete);
+
+ var referenceResponse = new OpenApiResponse
+ {
+ Reference = new OpenApiReference
+ {
+ Type = ReferenceType.Response,
+ Id = responseKey
+ }
+ };
+
+ foreach (var path in swaggerDoc.Paths)
+ foreach (var operation in path.Value.Operations)
+ operation.Value.Responses.TryAdd(responseKey, referenceResponse);
+ }
+
+ AddDefaultResponse(HttpStatusCode.BadRequest, new OpenApiResponse
+ {
+ Description = "A badly formatted request was made. See error message for details.",
+ Content = errorMessageContent,
+ });
+
+ AddDefaultResponse(HttpStatusCode.Unauthorized, new OpenApiResponse
+ {
+ Description = "No/invalid token provided."
+ });
+
+ AddDefaultResponse(HttpStatusCode.Forbidden, new OpenApiResponse
+ {
+ Description = "User lacks sufficient permissions for the operation."
+ });
+
+ AddDefaultResponse(HttpStatusCode.Conflict, new OpenApiResponse
+ {
+ Description = "A data integrity check failed while performing the operation. See error message for details.",
+ Content = errorMessageContent
+ });
+
+ AddDefaultResponse(HttpStatusCode.InternalServerError, new OpenApiResponse
+ {
+ Description = "The server encountered an unhandled error. See error message for details.",
+ Content = errorMessageContent
+ });
+
+ AddDefaultResponse(HttpStatusCode.ServiceUnavailable, new OpenApiResponse
+ {
+ Description = "The server may be starting up or shutting down."
+ });
+ }
+ }
+}
diff --git a/src/Tgstation.Server.Host/Controllers/UserController.cs b/src/Tgstation.Server.Host/Controllers/UserController.cs
index e41f4f7380..8988c62cbb 100644
--- a/src/Tgstation.Server.Host/Controllers/UserController.cs
+++ b/src/Tgstation.Server.Host/Controllers/UserController.cs
@@ -4,7 +4,6 @@ using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using System;
using System.Collections.Generic;
-using System.Globalization;
using System.Linq;
using System.Net;
using System.Threading;
@@ -19,10 +18,10 @@ using Tgstation.Server.Host.Security;
namespace Tgstation.Server.Host.Controllers
{
///
- /// For managing s
+ /// for managing s.
///
[Route(Routes.User)]
- public sealed class UserController : ModelController
+ public sealed class UserController : ApiController
{
///
/// The for the
@@ -53,7 +52,7 @@ namespace Tgstation.Server.Host.Controllers
/// The value of
/// The value of
/// The containing the value of
- public UserController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, ISystemIdentityFactory systemIdentityFactory, ICryptographySuite cryptographySuite, ILogger logger, IOptions generalConfigurationOptions) : base(databaseContext, authenticationContextFactory, logger, false)
+ public UserController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, ISystemIdentityFactory systemIdentityFactory, ICryptographySuite cryptographySuite, ILogger logger, IOptions generalConfigurationOptions) : base(databaseContext, authenticationContextFactory, logger, false, true)
{
this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
this.systemIdentityFactory = systemIdentityFactory ?? throw new ArgumentNullException(nameof(systemIdentityFactory));
@@ -61,9 +60,47 @@ namespace Tgstation.Server.Host.Controllers
generalConfiguration = generalConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(generalConfigurationOptions));
}
- ///
+ ///
+ /// Check if a given has a valid specified.
+ ///
+ /// The to check.
+ /// if is valid, a otherwise.
+ BadRequestObjectResult CheckValidName(UserUpdate model)
+ {
+ if (model.Name != null && model.Name.Contains(':', StringComparison.InvariantCulture))
+ return BadRequest(new ErrorMessage { Message = "Username must not contain colons!" });
+ return null;
+ }
+
+ ///
+ /// Attempt to change the password of a given .
+ ///
+ /// The to update.
+ /// The new password.
+ /// on success, if is too short.
+ BadRequestObjectResult TrySetPassword(Models.User dbUser, string newPassword)
+ {
+ if (newPassword.Length < generalConfiguration.MinimumPasswordLength)
+ return BadRequest(new ErrorMessage { Message = $"Password must be at least {generalConfiguration.MinimumPasswordLength} characters long!" });
+ cryptographySuite.SetUserPassword(dbUser, newPassword, true);
+ return null;
+ }
+
+ ///
+ /// Create a .
+ ///
+ /// The to create.
+ /// The for the operation.
+ /// A resulting in the of the operation.
+ /// created successfully.
+ /// The requested could not be loaded.
+ /// A system user was requested but this is not implemented on POSIX.
+ [HttpPut]
[TgsAuthorize(AdministrationRights.WriteUsers)]
- public override async Task Create([FromBody] UserUpdate model, CancellationToken cancellationToken)
+ [ProducesResponseType(typeof(Api.Models.User), 201)]
+ [ProducesResponseType(410)]
+ [ProducesResponseType(501)]
+ public async Task Create([FromBody] UserUpdate model, CancellationToken cancellationToken)
{
if (model == null)
throw new ArgumentNullException(nameof(model));
@@ -78,6 +115,10 @@ namespace Tgstation.Server.Host.Controllers
if (!(model.Name == null ^ model.SystemIdentifier == null))
return BadRequest(new ErrorMessage { Message = "User must have a name if and only if user has no system identifier!" });
+ var fail = CheckValidName(model);
+ if (fail != null)
+ return fail;
+
var dbUser = new Models.User
{
AdministrationRights = model.AdministrationRights ?? AdministrationRights.None,
@@ -107,9 +148,9 @@ namespace Tgstation.Server.Host.Controllers
}
else
{
- if (model.Password.Length < generalConfiguration.MinimumPasswordLength)
- return BadRequest(new ErrorMessage { Message = String.Format(CultureInfo.InvariantCulture, "Password must be at least {0} characters long!", generalConfiguration.MinimumPasswordLength) });
- cryptographySuite.SetUserPassword(dbUser, model.Password, true);
+ var result = TrySetPassword(dbUser, model.Password);
+ if (result != null)
+ return result;
}
dbUser.CanonicalName = dbUser.Name.ToUpperInvariant();
@@ -121,32 +162,48 @@ namespace Tgstation.Server.Host.Controllers
return StatusCode((int)HttpStatusCode.Created, dbUser.ToApi(true));
}
- ///
+ ///
+ /// Update a .
+ ///
+ /// The to update.
+ /// The for the operation.
+ /// A resulting in the of the operation.
+ /// updated successfully.
+ /// Requested does not exist.
+ [HttpPost]
[TgsAuthorize(AdministrationRights.WriteUsers | AdministrationRights.EditOwnPassword)]
- public override async Task Update([FromBody] UserUpdate model, CancellationToken cancellationToken)
+ [ProducesResponseType(typeof(Api.Models.User), 200)]
+ [ProducesResponseType(404)]
+ public async Task Update([FromBody] UserUpdate model, CancellationToken cancellationToken)
{
if (model == null)
throw new ArgumentNullException(nameof(model));
- var passwordEditOnly = !AuthenticationContext.User.AdministrationRights.Value.HasFlag(AdministrationRights.WriteUsers);
+ var callerAdministrationRights = (AdministrationRights)AuthenticationContext.GetRight(RightsType.Administration);
+ var passwordEditOnly = !callerAdministrationRights.HasFlag(AdministrationRights.WriteUsers);
- var originalUser = passwordEditOnly ? AuthenticationContext.User : await DatabaseContext.Users.Where(x => x.Id == model.Id)
- .Include(x => x.CreatedBy)
- .FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false);
+ var originalUser = passwordEditOnly
+ ? AuthenticationContext.User
+ : await DatabaseContext.Users.Where(x => x.Id == model.Id)
+ .Include(x => x.CreatedBy)
+ .FirstOrDefaultAsync(cancellationToken)
+ .ConfigureAwait(false);
if (originalUser == default)
return NotFound();
- if (passwordEditOnly && (model.Id != originalUser.Id || model.InstanceManagerRights.HasValue || model.AdministrationRights.HasValue || model.Enabled.HasValue || model.SystemIdentifier != null || model.Name != null))
+ // Ensure they are only trying to edit password (system identity change will trigger a bad request)
+ if (passwordEditOnly && (model.Id != originalUser.Id || model.InstanceManagerRights.HasValue || model.AdministrationRights.HasValue || model.Enabled.HasValue || model.Name != null))
return Forbid();
+ if (model.SystemIdentifier != null && model.SystemIdentifier != originalUser.SystemIdentifier)
+ return BadRequest(new ErrorMessage { Message = "Cannot change a user's system identifier!" });
+
if (model.Password != null)
{
- if (originalUser.PasswordHash == null)
- return BadRequest(new ErrorMessage { Message = "Cannot convert a system user to a password user!" });
- cryptographySuite.SetUserPassword(originalUser, model.Password, false);
+ var result = TrySetPassword(originalUser, model.Password);
+ if (result != null)
+ return result;
}
- else if (model.SystemIdentifier != null && model.SystemIdentifier != originalUser.SystemIdentifier)
- return BadRequest(new ErrorMessage { Message = "Cannot change a user's system identifier!" });
if (model.Name != null && model.Name.ToUpperInvariant() != originalUser.CanonicalName)
return BadRequest(new ErrorMessage { Message = "Can only change capitalization of a user's name!" });
@@ -154,23 +211,46 @@ namespace Tgstation.Server.Host.Controllers
originalUser.InstanceManagerRights = model.InstanceManagerRights ?? originalUser.InstanceManagerRights;
originalUser.AdministrationRights = model.AdministrationRights ?? originalUser.AdministrationRights;
originalUser.Enabled = model.Enabled ?? originalUser.Enabled;
+
+ var fail = CheckValidName(model);
+ if (fail != null)
+ return fail;
+
originalUser.Name = model.Name ?? originalUser.Name;
await DatabaseContext.Save(cancellationToken).ConfigureAwait(false);
- return Json(model.Id == originalUser.Id || (AuthenticationContext.GetRight(RightsType.Administration) & (ulong)AdministrationRights.ReadUsers) != 0 ? originalUser.ToApi(true) : new Api.Models.User
- {
- Id = originalUser.Id
- });
+ // return id only if not a self update or and cannot read users
+ return Json(
+ model.Id == originalUser.Id
+ || callerAdministrationRights.HasFlag(AdministrationRights.ReadUsers)
+ ? originalUser.ToApi(true)
+ : new Api.Models.User
+ {
+ Id = originalUser.Id
+ });
}
- ///
+ ///
+ /// Get information about the current .
+ ///
+ /// The of the operation.
+ /// The was retrieved successfully.
+ [HttpGet]
[TgsAuthorize]
- public override Task Read(CancellationToken cancellationToken) => Task.FromResult(Json(AuthenticationContext.User.ToApi(true)));
+ [ProducesResponseType(typeof(Api.Models.User), 200)]
+ public IActionResult Read() => Json(AuthenticationContext.User.ToApi(true));
- ///
+ ///
+ /// List all s in the server.
+ ///
+ /// The for the operation.
+ /// A resulting in the of the operation.
+ /// Retrieved s successfully.
+ [HttpGet(Routes.List)]
[TgsAuthorize(AdministrationRights.ReadUsers)]
- public override async Task List(CancellationToken cancellationToken)
+ [ProducesResponseType(typeof(IEnumerable), 200)]
+ public async Task List(CancellationToken cancellationToken)
{
var users = await DatabaseContext.Users
.Include(x => x.CreatedBy)
@@ -178,12 +258,22 @@ namespace Tgstation.Server.Host.Controllers
return Json(users.Select(x => x.ToApi(true)));
}
- ///
+ ///
+ /// Get a specific .
+ ///
+ /// The to retrieve.
+ /// The for the operation.
+ /// A resulting in the of the operation.
+ /// The was retrieved successfully.
+ /// The does not exist.
+ [HttpGet("{id}")]
[TgsAuthorize]
- public override async Task GetId(long id, CancellationToken cancellationToken)
+ [ProducesResponseType(typeof(Api.Models.User), 200)]
+ [ProducesResponseType(404)]
+ public async Task GetId(long id, CancellationToken cancellationToken)
{
if (id == AuthenticationContext.User.Id)
- return await Read(cancellationToken).ConfigureAwait(false);
+ return Read();
if (!((AdministrationRights)AuthenticationContext.GetRight(RightsType.Administration)).HasFlag(AdministrationRights.ReadUsers))
return Forbid();
diff --git a/src/Tgstation.Server.Host/Core/Application.cs b/src/Tgstation.Server.Host/Core/Application.cs
index 3e849b67a9..d3e7c9763b 100644
--- a/src/Tgstation.Server.Host/Core/Application.cs
+++ b/src/Tgstation.Server.Host/Core/Application.cs
@@ -5,12 +5,15 @@ using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Cors.Infrastructure;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Identity;
+using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Microsoft.Extensions.Primitives;
+using Microsoft.Net.Http.Headers;
+using Microsoft.OpenApi.Models;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using Serilog;
@@ -19,14 +22,16 @@ using Serilog.Formatting.Display;
using System;
using System.Globalization;
using System.IdentityModel.Tokens.Jwt;
-using System.Reflection;
+using System.Linq;
using System.Threading.Tasks;
+using Tgstation.Server.Api;
using Tgstation.Server.Host.Components;
using Tgstation.Server.Host.Components.Byond;
using Tgstation.Server.Host.Components.Chat;
using Tgstation.Server.Host.Components.Repository;
using Tgstation.Server.Host.Components.Watchdog;
using Tgstation.Server.Host.Configuration;
+using Tgstation.Server.Host.Controllers;
using Tgstation.Server.Host.IO;
using Tgstation.Server.Host.Models;
using Tgstation.Server.Host.Security;
@@ -34,7 +39,7 @@ using Tgstation.Server.Host.Security;
namespace Tgstation.Server.Host.Core
{
///
- #pragma warning disable CA1506
+#pragma warning disable CA1506
sealed class Application : IApplication
{
///
@@ -51,6 +56,11 @@ namespace Tgstation.Server.Host.Core
///
readonly IConfiguration configuration;
+ ///
+ /// The for the .
+ ///
+ readonly IAssemblyInformationProvider assemblyInformationProvider;
+
///
/// The for the
///
@@ -70,15 +80,20 @@ namespace Tgstation.Server.Host.Core
/// Construct an
///
/// The value of
+ /// The for the .
/// The value of
- public Application(IConfiguration configuration, Microsoft.AspNetCore.Hosting.IHostingEnvironment hostingEnvironment)
+ public Application(
+ IConfiguration configuration,
+ IAssemblyInformationProvider assemblyInformationProvider,
+ Microsoft.AspNetCore.Hosting.IHostingEnvironment hostingEnvironment)
{
this.configuration = configuration ?? throw new ArgumentNullException(nameof(configuration));
+ this.assemblyInformationProvider = assemblyInformationProvider ?? throw new ArgumentNullException(nameof(assemblyInformationProvider));
this.hostingEnvironment = hostingEnvironment ?? throw new ArgumentNullException(nameof(hostingEnvironment));
startupTcs = new TaskCompletionSource
bool Available { get; }
+ ///
+ /// Gets a that triggers if Crtl+C or an equivalent is pressed.
+ ///
+ CancellationToken CancelKeyPress { get; }
+
///
/// Write some to the
///
diff --git a/src/Tgstation.Server.Host/Models/CompileJob.cs b/src/Tgstation.Server.Host/Models/CompileJob.cs
index 3fadc5b182..c2398e017c 100644
--- a/src/Tgstation.Server.Host/Models/CompileJob.cs
+++ b/src/Tgstation.Server.Host/Models/CompileJob.cs
@@ -13,7 +13,7 @@ namespace Tgstation.Server.Host.Models
public Job Job { get; set; }
///
- /// The of
+ /// The of
///
public long JobId { get; set; }
diff --git a/src/Tgstation.Server.Host/Models/Migrations/DesignTimeDbContextFactoryHelpers.cs b/src/Tgstation.Server.Host/Models/Migrations/DesignTimeDbContextFactoryHelpers.cs
index cb4e730d1e..2de82a18bf 100644
--- a/src/Tgstation.Server.Host/Models/Migrations/DesignTimeDbContextFactoryHelpers.cs
+++ b/src/Tgstation.Server.Host/Models/Migrations/DesignTimeDbContextFactoryHelpers.cs
@@ -1,8 +1,8 @@
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Options;
-using System.IO;
-using System.Reflection;
using Tgstation.Server.Host.Configuration;
+using Tgstation.Server.Host.Core;
+using Tgstation.Server.Host.IO;
namespace Tgstation.Server.Host.Models.Migrations
{
@@ -28,7 +28,9 @@ namespace Tgstation.Server.Host.Models.Migrations
public static IOptions GetDbContextOptions()
{
var builder = new ConfigurationBuilder();
- builder.SetBasePath(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location));
+ var assemblyInfoProvider = new AssemblyInformationProvider();
+ var ioManager = new DefaultIOManager();
+ builder.SetBasePath(ioManager.GetDirectoryName(assemblyInfoProvider.Path));
builder.AddJsonFile(RootJson);
builder.AddJsonFile(DevJson);
var configuration = builder.Build();
diff --git a/src/Tgstation.Server.Host/Program.cs b/src/Tgstation.Server.Host/Program.cs
index 83895df62b..0e81f1d1f9 100644
--- a/src/Tgstation.Server.Host/Program.cs
+++ b/src/Tgstation.Server.Host/Program.cs
@@ -16,7 +16,7 @@ namespace Tgstation.Server.Host
/// The to use
///
#pragma warning disable SA1401 // Fields must be private
- internal static IServerFactory ServerFactory = new ServerFactory();
+ internal static IServerFactory ServerFactory = Host.ServerFactory.CreateDefault();
#pragma warning restore SA1401 // Fields must be private
///
diff --git a/src/Tgstation.Server.Host/Security/TokenFactory.cs b/src/Tgstation.Server.Host/Security/TokenFactory.cs
index 2974f6d3b1..3023189b84 100644
--- a/src/Tgstation.Server.Host/Security/TokenFactory.cs
+++ b/src/Tgstation.Server.Host/Security/TokenFactory.cs
@@ -2,7 +2,6 @@
using System;
using System.Globalization;
using System.IdentityModel.Tokens.Jwt;
-using System.Reflection;
using System.Security.Claims;
using System.Threading;
using System.Threading.Tasks;
@@ -42,7 +41,11 @@ namespace Tgstation.Server.Host.Security
///
/// The value of
/// The used for generating the
- public TokenFactory(IAsyncDelayer asyncDelayer, ICryptographySuite cryptographySuite)
+ /// The used to generate the issuer name.
+ public TokenFactory(
+ IAsyncDelayer asyncDelayer,
+ ICryptographySuite cryptographySuite,
+ IAssemblyInformationProvider assemblyInformationProvider)
{
ValidationParameters = new TokenValidationParameters
{
@@ -50,7 +53,7 @@ namespace Tgstation.Server.Host.Security
IssuerSigningKey = new SymmetricSecurityKey(cryptographySuite.GetSecureBytes(TokenSigningKeyByteAmount)),
ValidateIssuer = true,
- ValidIssuer = Assembly.GetExecutingAssembly().GetName().Name,
+ ValidIssuer = assemblyInformationProvider.Name.Name,
ValidateLifetime = true,
ValidateAudience = true,
diff --git a/src/Tgstation.Server.Host/Server.cs b/src/Tgstation.Server.Host/Server.cs
index 45a683a91e..7dcf93d719 100644
--- a/src/Tgstation.Server.Host/Server.cs
+++ b/src/Tgstation.Server.Host/Server.cs
@@ -30,6 +30,11 @@ namespace Tgstation.Server.Host
///
readonly IWebHostBuilder webHostBuilder;
+ ///
+ /// The for the .
+ ///
+ readonly IIOManager ioManager;
+
///
/// The s to run when the restarts
///
@@ -69,10 +74,12 @@ namespace Tgstation.Server.Host
/// Construct a
///
/// The value of
+ /// The value of .
/// The value of
- public Server(IWebHostBuilder webHostBuilder, string updatePath)
+ public Server(IWebHostBuilder webHostBuilder, IIOManager ioManager, string updatePath)
{
this.webHostBuilder = webHostBuilder ?? throw new ArgumentNullException(nameof(webHostBuilder));
+ this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager));
this.updatePath = updatePath;
webHostBuilder.ConfigureServices(serviceCollection => serviceCollection.AddSingleton(this));
diff --git a/src/Tgstation.Server.Host/ServerFactory.cs b/src/Tgstation.Server.Host/ServerFactory.cs
index d06f20fa93..57c46c1ae2 100644
--- a/src/Tgstation.Server.Host/ServerFactory.cs
+++ b/src/Tgstation.Server.Host/ServerFactory.cs
@@ -1,29 +1,65 @@
using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
using System;
using System.IO;
-using System.Reflection;
using Tgstation.Server.Host.Core;
+using Tgstation.Server.Host.IO;
namespace Tgstation.Server.Host
{
///
public sealed class ServerFactory : IServerFactory
{
+ ///
+ /// The for the .
+ ///
+ readonly IAssemblyInformationProvider assemblyInformationProvider;
+
+ ///
+ /// The for the .
+ ///
+ readonly IIOManager ioManager;
+
+ ///
+ /// Create the default .
+ ///
+ /// A new with the default settings.
+ public static IServerFactory CreateDefault()
+ => new ServerFactory(
+ new AssemblyInformationProvider(),
+ new DefaultIOManager());
+
+ ///
+ /// Initializes a new instance of the .
+ ///
+ /// The value of .
+ /// The value of .
+ internal ServerFactory(IAssemblyInformationProvider assemblyInformationProvider, IIOManager ioManager)
+ {
+ this.assemblyInformationProvider = assemblyInformationProvider ?? throw new ArgumentNullException(nameof(assemblyInformationProvider));
+ this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager));
+ }
+
///
public IServer CreateServer(string[] args, string updatePath)
{
var webHost = WebHost.CreateDefaultBuilder(args ?? throw new ArgumentNullException(nameof(args)))
.ConfigureAppConfiguration((context, configurationBuilder) => configurationBuilder.SetBasePath(Directory.GetCurrentDirectory()))
+ .ConfigureServices(serviceCollection =>
+ {
+ serviceCollection.AddSingleton(ioManager);
+ serviceCollection.AddSingleton(assemblyInformationProvider);
+ })
.UseStartup()
.SuppressStatusMessages(true)
.UseShutdownTimeout(TimeSpan.FromMinutes(1));
if(updatePath != null)
- webHost.UseContentRoot(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location));
+ webHost.UseContentRoot(Path.GetDirectoryName(assemblyInformationProvider.Path));
- return new Server(webHost, updatePath);
+ return new Server(webHost, ioManager, updatePath);
}
}
}
diff --git a/src/Tgstation.Server.Host/Tgstation.Server.Host.csproj b/src/Tgstation.Server.Host/Tgstation.Server.Host.csproj
index c7fb8a6fb4..2eba443cd5 100644
--- a/src/Tgstation.Server.Host/Tgstation.Server.Host.csproj
+++ b/src/Tgstation.Server.Host/Tgstation.Server.Host.csproj
@@ -83,6 +83,7 @@
allruntime; build; native; contentfiles; analyzers
+
diff --git a/tests/Tgstation.Server.Host.Tests/Components/Byond/TestPosixByondInstaller.cs b/tests/Tgstation.Server.Host.Tests/Components/Byond/TestPosixByondInstaller.cs
index e2fb2cff0f..b6337b1ebd 100644
--- a/tests/Tgstation.Server.Host.Tests/Components/Byond/TestPosixByondInstaller.cs
+++ b/tests/Tgstation.Server.Host.Tests/Components/Byond/TestPosixByondInstaller.cs
@@ -14,22 +14,22 @@ namespace Tgstation.Server.Host.Components.Byond.Tests
public void TestConstruction()
{
Assert.ThrowsException(() => new PosixByondInstaller(null, null, null));
- var mockIOManager = new Mock();
- Assert.ThrowsException(() => new PosixByondInstaller(mockIOManager.Object, null, null));
var mockPostWriteHandler = new Mock();
- Assert.ThrowsException(() => new PosixByondInstaller(mockIOManager.Object, mockPostWriteHandler.Object, null));
+ Assert.ThrowsException(() => new PosixByondInstaller(mockPostWriteHandler.Object, null, null));
+ var mockIOManager = new Mock();
+ Assert.ThrowsException(() => new PosixByondInstaller(mockPostWriteHandler.Object, mockIOManager.Object, null));
var mockLogger = new Mock>();
- new PosixByondInstaller(mockIOManager.Object, mockPostWriteHandler.Object, mockLogger.Object);
+ new PosixByondInstaller(mockPostWriteHandler.Object, mockIOManager.Object, mockLogger.Object);
}
[TestMethod]
public async Task TestCacheClean()
{
- var mockIOManager = new Mock();
var mockPostWriteHandler = new Mock();
+ var mockIOManager = new Mock();
var mockLogger = new Mock>();
- var installer = new PosixByondInstaller(mockIOManager.Object, mockPostWriteHandler.Object, mockLogger.Object);
+ var installer = new PosixByondInstaller(mockPostWriteHandler.Object, mockIOManager.Object, mockLogger.Object);
const string ByondCachePath = "~/.byond/cache";
@@ -37,7 +37,7 @@ namespace Tgstation.Server.Host.Components.Byond.Tests
await installer.CleanCache(default);
- mockIOManager.Verify();
+ mockPostWriteHandler.Verify();
mockIOManager.Setup(x => x.DeleteDirectory(ByondCachePath, default)).Throws(new OperationCanceledException()).Verifiable();
@@ -60,7 +60,7 @@ namespace Tgstation.Server.Host.Components.Byond.Tests
var mockIOManager = new Mock();
var mockPostWriteHandler = new Mock();
var mockLogger = new Mock>();
- var installer = new PosixByondInstaller(mockIOManager.Object, mockPostWriteHandler.Object, mockLogger.Object);
+ var installer = new PosixByondInstaller(mockPostWriteHandler.Object, mockIOManager.Object, mockLogger.Object);
await Assert.ThrowsExceptionAsync(() => installer.DownloadVersion(null, default)).ConfigureAwait(false);
@@ -79,7 +79,7 @@ namespace Tgstation.Server.Host.Components.Byond.Tests
var mockIOManager = new Mock();
var mockPostWriteHandler = new Mock();
var mockLogger = new Mock>();
- var installer = new PosixByondInstaller(mockIOManager.Object, mockPostWriteHandler.Object, mockLogger.Object);
+ var installer = new PosixByondInstaller(mockPostWriteHandler.Object, mockIOManager.Object, mockLogger.Object);
const string FakePath = "fake";
await Assert.ThrowsExceptionAsync(() => installer.InstallByond(null, null, default)).ConfigureAwait(false);
diff --git a/tests/Tgstation.Server.Host.Tests/Core/TestApplication.cs b/tests/Tgstation.Server.Host.Tests/Core/TestApplication.cs
index 94eba126e5..d19f8c3aa8 100644
--- a/tests/Tgstation.Server.Host.Tests/Core/TestApplication.cs
+++ b/tests/Tgstation.Server.Host.Tests/Core/TestApplication.cs
@@ -23,13 +23,18 @@ namespace Tgstation.Server.Host.Core.Tests
[TestMethod]
public void TestMethodThrows()
{
- Assert.ThrowsException(() => new Application(null, null));
+ Assert.ThrowsException(() => new Application(null, null, null));
var mockConfiguration = new Mock();
- Assert.ThrowsException(() => new Application(mockConfiguration.Object, null));
+ Assert.ThrowsException(() => new Application(mockConfiguration.Object, null, null));
+
+ var mockAssemblyInfo = new Mock();
+ mockAssemblyInfo.SetupGet(x => x.Name).Returns(typeof(Application).Assembly.GetName());
+
+ Assert.ThrowsException(() => new Application(mockConfiguration.Object, mockAssemblyInfo.Object, null));
var mockHostingEnvironment = new Mock();
- var app = new Application(mockConfiguration.Object, mockHostingEnvironment.Object);
+ var app = new Application(mockConfiguration.Object, mockAssemblyInfo.Object, mockHostingEnvironment.Object);
Assert.ThrowsException(() => app.ConfigureServices(null));
Assert.ThrowsException(() => app.Configure(null, null, null, null, null));
@@ -69,11 +74,11 @@ namespace Tgstation.Server.Host.Core.Tests
public void TestConfigureServicesThrowsWhenSetupWizardConfigurationDemands()
{
var mockConfiguration = new Mock();
- Assert.ThrowsException(() => new Application(mockConfiguration.Object, null));
-
+ var mockAssemblyInfo = new Mock();
+ mockAssemblyInfo.SetupGet(x => x.Name).Returns(typeof(Application).Assembly.GetName());
var mockHostingEnvironment = new Mock();
- var app = new Application(mockConfiguration.Object, mockHostingEnvironment.Object);
+ var app = new Application(mockConfiguration.Object, mockAssemblyInfo.Object, mockHostingEnvironment.Object);
var mockOptions = new Mock>();
mockOptions.SetupGet(x => x.Value).Returns(new GeneralConfiguration
diff --git a/tests/Tgstation.Server.Host.Tests/TestServerFactory.cs b/tests/Tgstation.Server.Host.Tests/TestServerFactory.cs
index 1334ae87c9..eede3ff41c 100644
--- a/tests/Tgstation.Server.Host.Tests/TestServerFactory.cs
+++ b/tests/Tgstation.Server.Host.Tests/TestServerFactory.cs
@@ -1,5 +1,8 @@
using Microsoft.VisualStudio.TestTools.UnitTesting;
+using Moq;
using System;
+using Tgstation.Server.Host.Core;
+using Tgstation.Server.Host.IO;
namespace Tgstation.Server.Host.Tests
{
@@ -9,10 +12,20 @@ namespace Tgstation.Server.Host.Tests
[TestClass]
public sealed class TestServerFactory
{
+ [TestMethod]
+ public void TestContructor()
+ {
+ Assert.ThrowsException(() => new ServerFactory(null, null));
+ IAssemblyInformationProvider assemblyInformationProvider = Mock.Of();
+ Assert.ThrowsException(() => new ServerFactory(assemblyInformationProvider, null));
+ IIOManager ioManager = Mock.Of();
+ new ServerFactory(assemblyInformationProvider, ioManager);
+ }
+
[TestMethod]
public void TestWorksWithoutUpdatePath()
{
- var factory = new ServerFactory();
+ var factory = ServerFactory.CreateDefault();
Assert.ThrowsException(() => factory.CreateServer(null, null));
factory.CreateServer(Array.Empty(), null);
@@ -21,7 +34,7 @@ namespace Tgstation.Server.Host.Tests
[TestMethod]
public void TestWorksWithUpdatePath()
{
- var factory = new ServerFactory();
+ var factory = ServerFactory.CreateDefault();
const string Path = "/test";
Assert.ThrowsException(() => factory.CreateServer(null, null));
diff --git a/tests/Tgstation.Server.Tests/IntegrationTest.cs b/tests/Tgstation.Server.Tests/IntegrationTest.cs
index d5b1fe13ef..355d25b031 100644
--- a/tests/Tgstation.Server.Tests/IntegrationTest.cs
+++ b/tests/Tgstation.Server.Tests/IntegrationTest.cs
@@ -29,7 +29,7 @@ namespace Tgstation.Server.Tests
try
{
var updatePath = Path.Combine(updatePathRoot, Guid.NewGuid().ToString());
- var server = new TestingServer(updatePath);
+ var server = new TestingServer(clientFactory, updatePath);
using (var serverCts = new CancellationTokenSource())
{
var cancellationToken = serverCts.Token;
@@ -98,7 +98,7 @@ namespace Tgstation.Server.Tests
[TestCategory("SkipWhenLiveUnitTesting")]
public async Task TestStandardOperation()
{
- var server = new TestingServer(null);
+ var server = new TestingServer(clientFactory, null);
using (var serverCts = new CancellationTokenSource())
{
var cancellationToken = serverCts.Token;
diff --git a/tests/Tgstation.Server.Tests/TestingServer.cs b/tests/Tgstation.Server.Tests/TestingServer.cs
index f75fbbea6e..29ece7cbca 100644
--- a/tests/Tgstation.Server.Tests/TestingServer.cs
+++ b/tests/Tgstation.Server.Tests/TestingServer.cs
@@ -3,8 +3,11 @@ using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
+using System.Net;
using System.Threading;
using System.Threading.Tasks;
+using Tgstation.Server.Api.Models;
+using Tgstation.Server.Client;
using Tgstation.Server.Host;
using Tgstation.Server.Host.Configuration;
@@ -19,8 +22,14 @@ namespace Tgstation.Server.Tests
readonly IServer realServer;
- public TestingServer(string updatePath)
+ readonly IServerClientFactory serverClientFactory;
+
+ readonly bool dumpOpenAPISpecpath;
+
+ public TestingServer(IServerClientFactory serverClientFactory, string updatePath)
{
+ this.serverClientFactory = serverClientFactory;
+
Directory = Path.GetTempFileName();
File.Delete(Directory);
System.IO.Directory.CreateDirectory(Directory);
@@ -31,6 +40,7 @@ namespace Tgstation.Server.Tests
var databaseType = Environment.GetEnvironmentVariable("TGS4_TEST_DATABASE_TYPE");
var connectionString = Environment.GetEnvironmentVariable("TGS4_TEST_CONNECTION_STRING");
var gitHubAccessToken = Environment.GetEnvironmentVariable("TGS4_TEST_GITHUB_TOKEN");
+ var dumpOpenAPISpecPathEnvVar = Environment.GetEnvironmentVariable("TGS4_TEST_DUMP_API_SPEC");
if (String.IsNullOrEmpty(databaseType))
Assert.Inconclusive("No database type configured in env var TGS4_TEST_DATABASE_TYPE!");
@@ -40,6 +50,8 @@ namespace Tgstation.Server.Tests
if (String.IsNullOrEmpty(gitHubAccessToken))
Console.WriteLine("WARNING: No GitHub access token configured, test may fail due to rate limits!");
+
+ dumpOpenAPISpecpath = !String.IsNullOrEmpty(dumpOpenAPISpecPathEnvVar);
var args = new List()
{
@@ -53,7 +65,10 @@ namespace Tgstation.Server.Tests
if (!String.IsNullOrEmpty(gitHubAccessToken))
args.Add(String.Format(CultureInfo.InvariantCulture, "General:GitHubAccessToken={0}", gitHubAccessToken));
- realServer = new ServerFactory().CreateServer(args.ToArray(), updatePath);
+ if (dumpOpenAPISpecpath)
+ Environment.SetEnvironmentVariable("ASPNETCORE_ENVIRONMENT", "Development");
+
+ realServer = ServerFactory.CreateDefault().CreateServer(args.ToArray(), updatePath);
}
public void Dispose()
@@ -61,6 +76,41 @@ namespace Tgstation.Server.Tests
System.IO.Directory.Delete(Directory, true);
}
- public Task RunAsync(CancellationToken cancellationToken) => realServer.RunAsync(cancellationToken);
+ public async Task RunAsync(CancellationToken cancellationToken)
+ {
+ Task runTask = realServer.RunAsync(cancellationToken);
+
+ if (dumpOpenAPISpecpath)
+ {
+ var giveUpAt = DateTimeOffset.Now.AddSeconds(60);
+ do
+ {
+ try
+ {
+ var client = await serverClientFactory.CreateServerClient(Url, User.AdminName, User.DefaultAdminPassword).ConfigureAwait(false);
+ break;
+ }
+ catch (ServiceUnavailableException)
+ {
+ //migrating, to be expected
+ if (DateTimeOffset.Now > giveUpAt)
+ throw;
+ await Task.Delay(TimeSpan.FromSeconds(1), cancellationToken);
+ }
+ } while (true);
+
+ // Dump swagger to disk
+ // This is purely for CI
+ var webRequest = WebRequest.Create(Url.ToString() + "swagger/v1/swagger.json");
+ using (var response = webRequest.GetResponse())
+ using (var content = response.GetResponseStream())
+ using (var output = new FileStream(@"C:\swagger.json", FileMode.Create))
+ {
+ await content.CopyToAsync(output);
+ }
+ }
+
+ await runTask;
+ }
}
}