mirror of
https://github.com/tgstation/tgstation-server.git
synced 2026-08-27 23:17:20 +01:00
Merge pull request #574 from Cyberboss/KeepOnKeepingOn
API/Client fixes, Integration Test, Work on fixing custom chat commands
This commit is contained in:
@@ -53,6 +53,9 @@ test_script:
|
||||
- vstest.console /logger:trx;LogFileName=results.trx "tests\Tgstation.Server.Host.Watchdog.Tests\bin\%CONFIGURATION%\netcoreapp2.0\Tgstation.Server.Host.Watchdog.Tests.dll" /Enablecodecoverage /inIsolation /Platform:x64
|
||||
- ps: $wc = New-Object 'System.Net.WebClient'
|
||||
- ps: $wc.UploadFile("https://ci.appveyor.com/api/testresults/mstest/$($env:APPVEYOR_JOB_ID)", (Resolve-Path .\TestResults\results.trx))
|
||||
- vstest.console /logger:trx;LogFileName=results.trx "tests\Tgstation.Server.Tests\bin\%CONFIGURATION%\netcoreapp2.0\Tgstation.Server.Tests.dll" /Enablecodecoverage /inIsolation /Platform:x64
|
||||
- ps: $wc = New-Object 'System.Net.WebClient'
|
||||
- ps: $wc.UploadFile("https://ci.appveyor.com/api/testresults/mstest/$($env:APPVEYOR_JOB_ID)", (Resolve-Path .\TestResults\results.trx))
|
||||
after_test:
|
||||
- ps: build/UploadCoverage.ps1
|
||||
- ps: build/BuildDox.ps1
|
||||
|
||||
@@ -29,6 +29,9 @@ RUN dotnet test -c Release
|
||||
WORKDIR /src/tests/Tgstation.Server.Host.Console.Tests
|
||||
RUN dotnet test -c Release
|
||||
|
||||
WORKDIR /src/tests/Tgstation.Server.Tests
|
||||
RUN dotnet test -c Release
|
||||
|
||||
WORKDIR /src/src/Tgstation.Server.Host.Console
|
||||
RUN dotnet publish -c Release -o /app
|
||||
|
||||
|
||||
+16
-6
@@ -52,7 +52,9 @@ An Authentication header is also required. See @ref api_auth
|
||||
TGS will only every return the response codes listed here
|
||||
|
||||
- 200: General OK status. Unless the HTTP DELETE verb was used to make the request (In which case the response body will be empty), the response body will contain a json model or array depending on the API called.
|
||||
- 400: Bad request made. The response body will contain a @ref Tgstation.Server.Api.Models.ErrorMessage model detailing the error
|
||||
- 201: Created: Returned when the request created an entity, 202 trumps this
|
||||
- 202: Accepted. Used when a response triggers a long running operation such as a @ref Tgstation.Server.Api.Models.Job or server restart
|
||||
- 400: Bad request made. The response body will contain an @ref Tgstation.Server.Api.Models.ErrorMessage model detailing the error
|
||||
- 401: User unauthorized. Invalid or expired credentials were provided. Check rights APIs for updates. See @ref api_auth for details
|
||||
- 403: Usage forbidden. User tried to make a request they were not allowed to perform.
|
||||
- 404: Not found. A resource was requested that had never existed. In the case of retrieving a resource by ID, it could potentially exist in the future
|
||||
@@ -60,9 +62,11 @@ TGS will only every return the response codes listed here
|
||||
- 408: Request timeout. The client took to long to continue a request
|
||||
- 409: Conflict. Documented in the requests that use them
|
||||
- 410: Gone. Attempted to access/modify a resource that ideally should have been ready, but isn't or no longer is
|
||||
- 422: Unprocessable Entity: Used specifically when an operation that requires a server restart is unable to be performed due to the @ref Tgstation.Server.Host.Watchdog not being present in the deployment. Blame MSO. Response body contains an @ref Tgstation.Server.Api.Models.ErrorMessage
|
||||
- 426: Upgrade required: Used when the client's API version is not compatible with the server's. Response body contains an @ref Tgstation.Server.Api.Models.ErrorMessage
|
||||
- 429: Rate limited. Used with operations that rely on GitHub.com. If a rate limit is hit for an operation this will be returned. Response will contain a Retry-After header
|
||||
- 500: Server error. Please report the request and response body to the code repository
|
||||
- 501: Not implemented. Currently used in two places: 1. Endpoints that trigger a server restart but the server is not running in a restartable configuration. 2.
|
||||
- 501: Not implemented. Functionality not available in the current server version
|
||||
- 503: Service unavailable. The server is either starting up or shutting down and isn't ready to respond to requests. You can try again soon and a response/lack thereof will indicate which of the two events it was
|
||||
|
||||
@section api_auth Authentication
|
||||
@@ -141,11 +145,11 @@ Users with the permission to modify @ref Tgstation.Server.Api.Models.Instance ob
|
||||
|
||||
@section api_ver Version
|
||||
|
||||
The version of TGS running can be retireved with this request
|
||||
The versions of the TGS host can be retireved with this request
|
||||
|
||||
GET "/" => Version
|
||||
GET "/" => @ref Tgstation.Server.Api.Models.ServerInformation
|
||||
|
||||
The Version model is based on the C# one and looks like this:
|
||||
The Version model fields are based on the C# one and looks like this:
|
||||
|
||||
@code{.json}
|
||||
{
|
||||
@@ -156,7 +160,7 @@ The Version model is based on the C# one and looks like this:
|
||||
}
|
||||
@endcode
|
||||
|
||||
Other fields may be present but should be ignored. See a description of these version numbers <a href="https://github.com/tgstation/tgstation-server/blob/master/.github/CONTRIBUTING.md#versioning">here</a>.
|
||||
Other fields may be present in the Version model but should be ignored. See a description of these version numbers <a href="https://github.com/tgstation/tgstation-server/blob/master/.github/CONTRIBUTING.md#versioning">here</a>.
|
||||
|
||||
@section api_admin Server-wide Administrative Actions
|
||||
|
||||
@@ -168,6 +172,8 @@ If you want to perform a live update of the server use
|
||||
|
||||
POST "/Administration" @ref Tgstation.Server.Api.Models.Administration => OK
|
||||
|
||||
With the @ref Tgstation.Server.Api.Models.Administration.NewVersion field set
|
||||
|
||||
Any DreamDaemon servers running will persist while the server installs the new version from the official tgstation-server GitHub (If it exists)
|
||||
|
||||
If the server is otherwise acting funky and you wish to restart it, use this request:
|
||||
@@ -390,6 +396,10 @@ If the path is empty, the root directory will be retrieved. The @ref Tgstation.S
|
||||
|
||||
If you do not have access to list the requested directory, a 403 response will be returned. If the path actually represents a file or the directory no longer exists a 410 response will be returned.
|
||||
|
||||
To create an empty config directory use the following request
|
||||
|
||||
I PUT "/Config" @ref Tgstation.Server.Api.Models.ConfigurationFile => @ref Tgstation.Server.Api.Models.ConfigurationFile
|
||||
|
||||
To get the content of a static file use the following method
|
||||
|
||||
I GET "/Config/File/<path to file>" => @ref Tgstation.Server.Api.Models.ConfigurationFile
|
||||
|
||||
+2
-17
@@ -135,8 +135,8 @@
|
||||
switch(command)
|
||||
if(TGS4_TOPIC_CHAT_COMMAND)
|
||||
var/result = HandleCustomCommand(params[TGS4_PARAMETER_DATA])
|
||||
if(!result)
|
||||
return json_encode(list("error" = "Error running chat command!"))
|
||||
if(result == null)
|
||||
result = "Error running chat command!"
|
||||
return result
|
||||
if(TGS4_TOPIC_EVENT)
|
||||
intercepted_message_queue = list()
|
||||
@@ -294,21 +294,6 @@
|
||||
channel.is_private_channel = channel_json["isPrivateChannel"] || FALSE
|
||||
return channel
|
||||
|
||||
#undef TGS4_TOPIC_COMMAND
|
||||
#undef TGS4_TOPIC_TOKEN
|
||||
#undef TGS4_TOPIC_SUCCESS
|
||||
#undef TGS4_TOPIC_SWAP
|
||||
#undef TGS4_TOPIC_SWAP_DELAYED
|
||||
#undef TGS4_TOPIC_CHAT_COMMAND
|
||||
#undef TGS4_TOPIC_EVENT
|
||||
|
||||
#undef TGS4_COMM_SERVER_PRIMED
|
||||
#undef TGS4_COMM_SERVER_REBOOT
|
||||
#undef TGS4_COMM_END_PROCESS
|
||||
#undef TGS4_COMM_CHAT
|
||||
|
||||
#undef TGS4_COMM_VALIDATE
|
||||
|
||||
/*
|
||||
The MIT License
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
var/datum/other = custom_commands[command_name]
|
||||
TGS_ERROR_LOG("Custom commands [other.type] and [I] have the same name (\"[command_name]\"), only [other.type] will be available!")
|
||||
continue
|
||||
results[command_name] = list("help_text" = stc.help_text, "admin_only" = stc.admin_only)
|
||||
results += list(list("name" = command_name, "help_text" = stc.help_text, "admin_only" = stc.admin_only))
|
||||
custom_commands[command_name] = stc
|
||||
|
||||
var/commands_file = chat_commands_json_path
|
||||
@@ -33,8 +33,12 @@
|
||||
u.channel = DecodeChannel(user["channel"])
|
||||
|
||||
var/datum/tgs_chat_command/sc = custom_commands[command]
|
||||
var/result = sc.Run(u, params)
|
||||
return json_encode(list("result" = result))
|
||||
if(sc)
|
||||
var/result = sc.Run(u, params)
|
||||
if(result == null)
|
||||
result = ""
|
||||
return result
|
||||
return "Unknown command: [command]!"
|
||||
|
||||
/*
|
||||
|
||||
|
||||
@@ -48,7 +48,12 @@ namespace Tgstation.Server.Api
|
||||
/// <summary>
|
||||
/// The current <see cref="AssemblyName"/>
|
||||
/// </summary>
|
||||
internal static readonly AssemblyName assemblyName = Assembly.GetExecutingAssembly().GetName();
|
||||
static readonly AssemblyName assemblyName = Assembly.GetExecutingAssembly().GetName();
|
||||
|
||||
/// <summary>
|
||||
/// Get the version of the <see cref="Api"/> the caller is using
|
||||
/// </summary>
|
||||
public static Version Version => assemblyName.Version;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="Models.Instance.Id"/> being accessed
|
||||
@@ -90,11 +95,7 @@ namespace Tgstation.Server.Api
|
||||
/// </summary>
|
||||
/// <param name="otherVersion">The <see cref="Version"/> to test</param>
|
||||
/// <returns><see langword="true"/> if the given version is compatible with the API. <see langword="false"/> otherwise</returns>
|
||||
public static bool CheckCompatibility(Version otherVersion)
|
||||
{
|
||||
var ourVersion = assemblyName.Version;
|
||||
return !(ourVersion.Major != otherVersion.Major || ourVersion.Minor != otherVersion.Minor || ourVersion.Build > otherVersion.Build);
|
||||
}
|
||||
public static bool CheckCompatibility(Version otherVersion) => !(Version.Major != otherVersion.Major || Version.Minor != otherVersion.Minor || Version.Build > otherVersion.Build);
|
||||
|
||||
/// <summary>
|
||||
/// Construct <see cref="ApiHeaders"/> for JWT authentication
|
||||
@@ -144,7 +145,7 @@ namespace Tgstation.Server.Api
|
||||
|
||||
//make sure the api header matches ours
|
||||
if (!requestHeaders.Headers.TryGetValue(ApiVersionHeader, out var apiUserAgentHeaderValues) || !ProductInfoHeaderValue.TryParse(apiUserAgentHeaderValues.FirstOrDefault(), out var apiUserAgent) || apiUserAgent.Product.Name != assemblyName.Name)
|
||||
throw new InvalidOperationException("Missing API user agent!");
|
||||
throw new InvalidOperationException("Missing API version!");
|
||||
|
||||
if (!Version.TryParse(apiUserAgent.Product.Version, out var apiVersion))
|
||||
throw new InvalidOperationException("Malformed API version!");
|
||||
@@ -152,9 +153,6 @@ namespace Tgstation.Server.Api
|
||||
ApiVersion = apiVersion;
|
||||
UserAgent = clientUserAgent.Product;
|
||||
|
||||
if(!CheckCompatibility(ApiVersion))
|
||||
throw new InvalidOperationException(String.Format(CultureInfo.InvariantCulture, "Given API version is incompatible with version {0}!", ApiVersion));
|
||||
|
||||
if (!requestHeaders.Headers.TryGetValue(HeaderNames.Authorization, out StringValues authorization))
|
||||
throw new InvalidOperationException(String.Format(CultureInfo.InvariantCulture, "Missing {0} header!", HeaderNames.Authorization));
|
||||
var auth = authorization.First();
|
||||
@@ -209,9 +207,15 @@ namespace Tgstation.Server.Api
|
||||
Token = token;
|
||||
Username = username;
|
||||
Password = password;
|
||||
ApiVersion = assemblyName.Version;
|
||||
ApiVersion = Version;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if the <see cref="ApiVersion"/> is compatible with <see cref="Version"/>
|
||||
/// </summary>
|
||||
/// <returns><see langword="true"/> if the API is compatible, <see langword="false"/> otherwise</returns>
|
||||
public bool Compatible() => CheckCompatibility(ApiVersion);
|
||||
|
||||
/// <summary>
|
||||
/// Set <see cref="HttpRequestHeaders"/> using the <see cref="ApiHeaders"/>. This initially clears <paramref name="headers"/>
|
||||
/// </summary>
|
||||
@@ -234,7 +238,7 @@ namespace Tgstation.Server.Api
|
||||
headers.Add(usernameHeader, Username);
|
||||
}
|
||||
headers.UserAgent.Add(new ProductInfoHeaderValue(UserAgent));
|
||||
headers.Add(ApiVersionHeader, ApiVersion.ToString());
|
||||
headers.Add(ApiVersionHeader, new ProductHeaderValue(assemblyName.Name, ApiVersion.ToString()).ToString());
|
||||
instanceId = instanceId ?? InstanceId;
|
||||
if (instanceId.HasValue)
|
||||
headers.Add(instanceIdHeader, instanceId.ToString());
|
||||
|
||||
@@ -19,7 +19,7 @@ namespace Tgstation.Server.Api.Models
|
||||
public Uri TrackedRepositoryUrl { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The latest available version of the Tgstation.Server.Host assembly from the upstream repository. If <see cref="Version.Minor"/> is higher than <see cref="CurrentVersion"/>'s the update cannot be applied due to API changes
|
||||
/// The latest available version of the Tgstation.Server.Host assembly from the upstream repository. If <see cref="Version.Minor"/> is higher than <see cref="NewVersion"/>'s the update cannot be applied due to API changes
|
||||
/// </summary>
|
||||
[Permissions(DenyWrite = true)]
|
||||
public Version LatestVersion { get; set; }
|
||||
@@ -28,6 +28,6 @@ namespace Tgstation.Server.Api.Models
|
||||
/// Changes the version of Tgstation.Server.Host to the given version from the upstream repository
|
||||
/// </summary>
|
||||
[Permissions(WriteRight = AdministrationRights.ChangeVersion)]
|
||||
public Version CurrentVersion { get; set; }
|
||||
public Version NewVersion { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,6 @@ namespace Tgstation.Server.Api.Models
|
||||
/// <summary>
|
||||
/// The version of the API the server is using
|
||||
/// </summary>
|
||||
public Version SeverApiVersion { get; set; } = ApiHeaders.assemblyName.Version;
|
||||
public Version SeverApiVersion { get; set; } = ApiHeaders.Version;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
using System;
|
||||
|
||||
namespace Tgstation.Server.Api.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents basic server information
|
||||
/// </summary>
|
||||
public sealed class ServerInformation
|
||||
{
|
||||
/// <summary>
|
||||
/// The version of the host
|
||||
/// </summary>
|
||||
public Version Version { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="Api"/> version of the host
|
||||
/// </summary>
|
||||
public Version ApiVersion { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,16 @@
|
||||
/// <inheritdoc />
|
||||
public class User : Internal.User
|
||||
{
|
||||
/// <summary>
|
||||
/// The name of the default admin user
|
||||
/// </summary>
|
||||
public const string AdminName = "Admin";
|
||||
|
||||
/// <summary>
|
||||
/// The default admin password
|
||||
/// </summary>
|
||||
public const string DefaultAdminPassword = "ISolemlySwearToDeleteTheDataDirectory";
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="User"/> who created this <see cref="User"/>
|
||||
/// </summary>
|
||||
|
||||
@@ -21,7 +21,7 @@ namespace Tgstation.Server.Api.Rights
|
||||
/// </summary>
|
||||
RestartHost = 2,
|
||||
/// <summary>
|
||||
/// User can change <see cref="Models.Administration.CurrentVersion"/>
|
||||
/// User can change <see cref="Models.Administration.NewVersion"/>
|
||||
/// </summary>
|
||||
ChangeVersion = 4,
|
||||
/// <summary>
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
<FileVersion>4.0.0.0</FileVersion>
|
||||
<PackageTags>json web api tgstation-server tgstation ss13 byond</PackageTags>
|
||||
<PackageReleaseNotes>Prototype release</PackageReleaseNotes>
|
||||
<Version>4.0.0.0-preview1</Version>
|
||||
<Version>4.0.0.0-preview2</Version>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
|
||||
|
||||
@@ -5,6 +5,7 @@ using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Tgstation.Server.Api;
|
||||
@@ -83,7 +84,7 @@ namespace Tgstation.Server.Client
|
||||
};
|
||||
|
||||
if (body != null)
|
||||
message.Content = new StringContent(JsonConvert.SerializeObject(body, serializerSettings));
|
||||
message.Content = new StringContent(JsonConvert.SerializeObject(body, serializerSettings), Encoding.UTF8, ApiHeaders.ApplicationJson);
|
||||
|
||||
Headers.SetRequestHeaders(message.Headers, instanceId);
|
||||
|
||||
@@ -95,22 +96,20 @@ namespace Tgstation.Server.Client
|
||||
|
||||
var json = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
|
||||
|
||||
if (!response.IsSuccessStatusCode) {
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
ErrorMessage errorMessage = null;
|
||||
try
|
||||
{
|
||||
//check if json serializes to an error message
|
||||
errorMessage = JsonConvert.DeserializeObject<ErrorMessage>(json, serializerSettings);
|
||||
}
|
||||
catch (JsonSerializationException) { }
|
||||
catch (JsonException) { }
|
||||
|
||||
switch (response.StatusCode)
|
||||
{
|
||||
case HttpStatusCode.BadRequest:
|
||||
//validate our api version is compatible
|
||||
if(errorMessage != null && ApiHeaders.CheckCompatibility(errorMessage.SeverApiVersion))
|
||||
throw new ApiMismatchException(errorMessage);
|
||||
goto default;
|
||||
case HttpStatusCode.UpgradeRequired:
|
||||
throw new ApiMismatchException(errorMessage);
|
||||
case HttpStatusCode.Unauthorized:
|
||||
throw new UnauthorizedException();
|
||||
case HttpStatusCode.RequestTimeout:
|
||||
@@ -124,10 +123,11 @@ namespace Tgstation.Server.Client
|
||||
case HttpStatusCode.Conflict:
|
||||
throw new ConflictException(errorMessage, response.StatusCode);
|
||||
case HttpStatusCode.NotImplemented:
|
||||
case (HttpStatusCode)422: //unprocessable entity
|
||||
throw new MethodNotSupportedException();
|
||||
case HttpStatusCode.InternalServerError:
|
||||
//response
|
||||
throw new ServerErrorException(json); //json is html
|
||||
throw new ServerErrorException(json); //json is html
|
||||
case (HttpStatusCode)429: //rate limited
|
||||
response.Headers.TryGetValues("Retry-After", out var values);
|
||||
throw new RateLimitException(values?.FirstOrDefault());
|
||||
|
||||
@@ -14,7 +14,11 @@ namespace Tgstation.Server.Client
|
||||
/// </summary>
|
||||
/// <param name="errorMessage">The <see cref="ErrorMessage"/> for the <see cref="ClientException"/></param>
|
||||
/// <param name="statusCode">The <see cref="HttpStatusCode"/> for the <see cref="ClientException"/></param>
|
||||
public ApiConflictException(ErrorMessage errorMessage, HttpStatusCode statusCode) : base(errorMessage, statusCode) { }
|
||||
public ApiConflictException(ErrorMessage errorMessage, HttpStatusCode statusCode) : base(errorMessage ?? new ErrorMessage
|
||||
{
|
||||
Message = "An unknown API error occurred!",
|
||||
SeverApiVersion = null
|
||||
}, statusCode) { }
|
||||
|
||||
/// <summary>
|
||||
/// Construct an <see cref="ApiConflictException"/>
|
||||
|
||||
@@ -36,9 +36,9 @@ namespace Tgstation.Server.Client
|
||||
IUsersClient Users { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="System.Version"/> of the <see cref="IServerClient"/>
|
||||
/// The <see cref="ServerInformation"/> of the <see cref="IServerClient"/>
|
||||
/// </summary>
|
||||
Task<Version> Version(CancellationToken cancellationToken);
|
||||
Task<ServerInformation> Version(CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Adds a <paramref name="requestLogger"/> to the request pipeline
|
||||
|
||||
@@ -55,7 +55,7 @@ namespace Tgstation.Server.Client
|
||||
public void Dispose() => apiClient.Dispose();
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<Version> Version(CancellationToken cancellationToken) => apiClient.Read<Version>(Routes.Root, cancellationToken);
|
||||
public Task<ServerInformation> Version(CancellationToken cancellationToken) => apiClient.Read<ServerInformation>(Routes.Root, cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public void AddRequestLogger(IRequestLogger requestLogger) => apiClient.AddRequestLogger(requestLogger);
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netstandard2.0</TargetFramework>
|
||||
<DebugType>Full</DebugType>
|
||||
<Version>4.0.0.0-preview1</Version>
|
||||
<Version>4.0.0.0-preview2</Version>
|
||||
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
|
||||
<Authors>Cyberboss</Authors>
|
||||
<Company>/tg/station 13</Company>
|
||||
|
||||
@@ -44,7 +44,13 @@ namespace Tgstation.Server.Host.Components.Chat
|
||||
{
|
||||
var resultBytes = await ioManager.ReadAllBytes(commandsPath, cancellationToken).ConfigureAwait(false);
|
||||
var resultJson = Encoding.UTF8.GetString(resultBytes);
|
||||
var result = JsonConvert.DeserializeObject<List<CustomCommand>>(resultJson);
|
||||
var result = JsonConvert.DeserializeObject<List<CustomCommand>>(resultJson, new JsonSerializerSettings
|
||||
{
|
||||
ContractResolver = new DefaultContractResolver
|
||||
{
|
||||
NamingStrategy = new SnakeCaseNamingStrategy()
|
||||
}
|
||||
});
|
||||
foreach (var I in result)
|
||||
I.SetHandler(customCommandHandler);
|
||||
return result;
|
||||
@@ -61,10 +67,7 @@ namespace Tgstation.Server.Host.Components.Chat
|
||||
using (await SemaphoreSlimContext.Lock(channelsSemaphore, cancellationToken).ConfigureAwait(false))
|
||||
await ioManager.WriteAllBytes(channelsPath, Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(channels, Formatting.Indented, new JsonSerializerSettings
|
||||
{
|
||||
ContractResolver = new DefaultContractResolver
|
||||
{
|
||||
NamingStrategy = new CamelCaseNamingStrategy()
|
||||
}
|
||||
ContractResolver = new CamelCasePropertyNamesContractResolver()
|
||||
})), cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -364,7 +364,7 @@ namespace Tgstation.Server.Host.Components.Compiler
|
||||
{
|
||||
//server never validated or compile failed
|
||||
await eventConsumer.HandleEvent(EventType.CompileFailure, new List<string> { resolvedGameDirectory, exitCode == 0 ? "1" : "0" }, cancellationToken).ConfigureAwait(false);
|
||||
throw new JobException(exitCode == 0 ? "Validation of the TGS api failed!" : String.Format(CultureInfo.InvariantCulture, "DM exited with a non-zero code: {0}{1}", exitCode, job.Output));
|
||||
throw new JobException(exitCode == 0 ? "Validation of the TGS api failed!" : String.Format(CultureInfo.InvariantCulture, "DM exited with a non-zero code: {0}{1}{2}", exitCode, Environment.NewLine, job.Output));
|
||||
}
|
||||
|
||||
logger.LogTrace("Running post compile event...");
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
using Tgstation.Server.Host.Components.Chat;
|
||||
|
||||
namespace Tgstation.Server.Host.Components.Interop
|
||||
{
|
||||
sealed class ChatCommand
|
||||
{
|
||||
public string Command { get; set; }
|
||||
public string Parameters { get; set; }
|
||||
public User User { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -347,6 +347,22 @@ namespace Tgstation.Server.Host.Components.StaticFiles
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<bool> CreateDirectory(string configurationRelativePath, ISystemIdentity systemIdentity, CancellationToken cancellationToken)
|
||||
{
|
||||
await EnsureDirectories(cancellationToken).ConfigureAwait(false);
|
||||
var path = ValidateConfigRelativePath(configurationRelativePath);
|
||||
|
||||
bool? result = null;
|
||||
void DoCreate() => result = synchronousIOManager.CreateDirectory(path, cancellationToken);
|
||||
if (systemIdentity == null)
|
||||
await Task.Factory.StartNew(DoCreate, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current).ConfigureAwait(false);
|
||||
else
|
||||
await systemIdentity.RunImpersonated(DoCreate, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return result.Value;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task StartAsync(CancellationToken cancellationToken) => EnsureDirectories(cancellationToken);
|
||||
|
||||
|
||||
@@ -48,6 +48,15 @@ namespace Tgstation.Server.Host.Components.StaticFiles
|
||||
/// <returns>A <see cref="Task{TResult}"/> resulting in the <see cref="Api.Models.ConfigurationFile"/> of the file</returns>
|
||||
Task<ConfigurationFile> Read(string configurationRelativePath, ISystemIdentity systemIdentity, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Create an empty directory at <paramref name="configurationRelativePath"/>
|
||||
/// </summary>
|
||||
/// <param name="configurationRelativePath">The relative path in the Configuration directory</param>
|
||||
/// <param name="systemIdentity">The <see cref="ISystemIdentity"/> for the operation. If <see langword="null"/>, the operation will be performed as the user of the <see cref="Core.Application"/></param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation. Usage may result in partial writes</param>
|
||||
/// <returns>A <see cref="Task{TResult}"/> resulting in <see langword="true"/> if the directory already existed, <see langword="false"/> otherwise</returns>
|
||||
Task<bool> CreateDirectory(string configurationRelativePath, ISystemIdentity systemIdentity, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Writes to a given <paramref name="configurationRelativePath"/>
|
||||
/// </summary>
|
||||
|
||||
@@ -153,10 +153,7 @@ namespace Tgstation.Server.Host.Components.Watchdog
|
||||
|
||||
var interopJson = JsonConvert.SerializeObject(interopInfo, Formatting.Indented, new JsonSerializerSettings
|
||||
{
|
||||
ContractResolver = new DefaultContractResolver
|
||||
{
|
||||
NamingStrategy = new CamelCaseNamingStrategy()
|
||||
},
|
||||
ContractResolver = new CamelCasePropertyNamesContractResolver(),
|
||||
ReferenceLoopHandling = ReferenceLoopHandling.Ignore
|
||||
});
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using Byond.TopicSender;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Serialization;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
@@ -858,7 +859,19 @@ namespace Tgstation.Server.Host.Components.Watchdog
|
||||
if (!Running)
|
||||
return "ERROR: Server offline!";
|
||||
|
||||
var command = String.Format(CultureInfo.InvariantCulture, "{0}&{1}={2}", byondTopicSender.SanitizeString(Constants.DMTopicChatCommand), byondTopicSender.SanitizeString(Constants.DMParameterData), byondTopicSender.SanitizeString(JsonConvert.SerializeObject(arguments)));
|
||||
var commandObject = new ChatCommand
|
||||
{
|
||||
Command = commandName,
|
||||
Parameters = arguments,
|
||||
User = sender
|
||||
};
|
||||
|
||||
var json = JsonConvert.SerializeObject(arguments, new JsonSerializerSettings
|
||||
{
|
||||
ContractResolver = new CamelCasePropertyNamesContractResolver()
|
||||
});
|
||||
|
||||
var command = String.Format(CultureInfo.InvariantCulture, "{0}&{1}={2}", byondTopicSender.SanitizeString(Constants.DMTopicChatCommand), byondTopicSender.SanitizeString(Constants.DMParameterData), byondTopicSender.SanitizeString(json));
|
||||
|
||||
var activeServer = AlphaIsActive ? alphaServer : bravoServer;
|
||||
return await activeServer.SendCommand(command, cancellationToken).ConfigureAwait(false) ?? "ERROR: Bad topic exchange!";
|
||||
|
||||
@@ -28,10 +28,7 @@ namespace Tgstation.Server.Host.Controllers
|
||||
[Route(Routes.Administration)]
|
||||
public sealed class AdministrationController : ModelController<Administration>
|
||||
{
|
||||
/// <summary>
|
||||
/// HTTP 429 status code
|
||||
/// </summary>
|
||||
const int RateLimitHttpStatusCode = 429;
|
||||
const string RestartNotSupportedException = "This deployment of tgstation-server is lacking the Tgstation.Server.Host.Watchdog component. Restarts and version changes cannot be completed!";
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="IGitHubClient"/> for the <see cref="AdministrationController"/>
|
||||
@@ -83,38 +80,44 @@ namespace Tgstation.Server.Host.Controllers
|
||||
Logger.LogWarning("Exceeded GitHub rate limit!");
|
||||
var secondsString = Math.Ceiling((exception.Reset - DateTimeOffset.Now).TotalSeconds).ToString(CultureInfo.InvariantCulture);
|
||||
Response.Headers.Add("Retry-After", new StringValues(secondsString));
|
||||
return StatusCode(RateLimitHttpStatusCode);
|
||||
return StatusCode(429);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
[TgsAuthorize]
|
||||
public override async Task<IActionResult> Read(CancellationToken cancellationToken)
|
||||
{
|
||||
var model = new Administration
|
||||
{
|
||||
CurrentVersion = application.Version
|
||||
};
|
||||
try
|
||||
{
|
||||
var repositoryTask = gitHubClient.Repository.Get(updatesConfiguration.GitHubRepositoryId);
|
||||
var releases = (await gitHubClient.Repository.Release.GetAll(updatesConfiguration.GitHubRepositoryId).ConfigureAwait(false)).Where(x => x.TagName.StartsWith(updatesConfiguration.GitTagPrefix, StringComparison.InvariantCulture));
|
||||
|
||||
Version greatestVersion = null;
|
||||
foreach (var I in releases)
|
||||
if (Version.TryParse(I.TagName.Replace(updatesConfiguration.GitTagPrefix, String.Empty, StringComparison.Ordinal), out var version)
|
||||
&& version.Major == application.Version.Major
|
||||
&& (greatestVersion == null || version > greatestVersion))
|
||||
greatestVersion = version;
|
||||
Uri repoUrl = null;
|
||||
try
|
||||
{
|
||||
var repositoryTask = gitHubClient.Repository.Get(updatesConfiguration.GitHubRepositoryId);
|
||||
var releases = (await gitHubClient.Repository.Release.GetAll(updatesConfiguration.GitHubRepositoryId).ConfigureAwait(false)).Where(x => x.TagName.StartsWith(updatesConfiguration.GitTagPrefix, StringComparison.InvariantCulture));
|
||||
|
||||
model.LatestVersion = greatestVersion;
|
||||
model.TrackedRepositoryUrl = new Uri((await repositoryTask.ConfigureAwait(false)).HtmlUrl);
|
||||
model.WindowsHost = RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
|
||||
foreach (var I in releases)
|
||||
if (Version.TryParse(I.TagName.Replace(updatesConfiguration.GitTagPrefix, String.Empty, StringComparison.Ordinal), out var version)
|
||||
&& version.Major == application.Version.Major
|
||||
&& (greatestVersion == null || version > greatestVersion))
|
||||
greatestVersion = version;
|
||||
repoUrl = new Uri((await repositoryTask.ConfigureAwait(false)).HtmlUrl);
|
||||
}
|
||||
catch (NotFoundException e)
|
||||
{
|
||||
Logger.LogWarning("Not found exception while retrieving upstream repository info: {0}", e);
|
||||
}
|
||||
return Json(new Administration
|
||||
{
|
||||
LatestVersion = greatestVersion,
|
||||
TrackedRepositoryUrl = repoUrl,
|
||||
WindowsHost = RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
|
||||
});
|
||||
}
|
||||
catch (RateLimitExceededException e)
|
||||
{
|
||||
return RateLimit(e);
|
||||
}
|
||||
return Json(model);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -124,10 +127,10 @@ namespace Tgstation.Server.Host.Controllers
|
||||
if (model == null)
|
||||
throw new ArgumentNullException(nameof(model));
|
||||
|
||||
if (model.CurrentVersion == null)
|
||||
if (model.NewVersion == null)
|
||||
return BadRequest(new ErrorMessage { Message = "Missing new version!" });
|
||||
|
||||
if (model.CurrentVersion.Major != application.Version.Major)
|
||||
if (model.NewVersion.Major != application.Version.Major)
|
||||
return BadRequest(new ErrorMessage { Message = "Cannot update to a different suite version!" });
|
||||
|
||||
IEnumerable<Release> releases;
|
||||
@@ -141,7 +144,7 @@ namespace Tgstation.Server.Host.Controllers
|
||||
}
|
||||
|
||||
foreach (var release in releases)
|
||||
if (Version.TryParse(release.TagName.Replace(updatesConfiguration.GitTagPrefix, String.Empty, StringComparison.Ordinal), out var version) && version == model.CurrentVersion)
|
||||
if (Version.TryParse(release.TagName.Replace(updatesConfiguration.GitTagPrefix, String.Empty, StringComparison.Ordinal), out var version) && version == model.NewVersion)
|
||||
{
|
||||
var asset = release.Assets.Where(x => x.Name == updatesConfiguration.UpdatePackageAssetName).FirstOrDefault();
|
||||
if (asset == default)
|
||||
@@ -151,10 +154,16 @@ namespace Tgstation.Server.Host.Controllers
|
||||
try
|
||||
{
|
||||
if (!await serverUpdater.ApplyUpdate(assetBytes, ioManager, cancellationToken).ConfigureAwait(false))
|
||||
return StatusCode((int)HttpStatusCode.NotImplemented);
|
||||
return UnprocessableEntity(new ErrorMessage
|
||||
{
|
||||
Message = RestartNotSupportedException
|
||||
}); //unprocessable entity
|
||||
}
|
||||
catch (InvalidOperationException) { } //we were beat to the punch
|
||||
return Ok(); //gtfo of here before all the cancellation tokens fire
|
||||
catch (InvalidOperationException)
|
||||
{
|
||||
return StatusCode((int)HttpStatusCode.ServiceUnavailable); //we were beat to the punch, really shouldn't happen but heat death of the universe and what not
|
||||
}
|
||||
return Accepted(); //gtfo of here before all the cancellation tokens fire
|
||||
}
|
||||
|
||||
return StatusCode((int)HttpStatusCode.Gone);
|
||||
@@ -163,6 +172,18 @@ namespace Tgstation.Server.Host.Controllers
|
||||
/// <inheritdoc />
|
||||
[HttpDelete]
|
||||
[TgsAuthorize(AdministrationRights.RestartHost)]
|
||||
public Task<IActionResult> Delete() => Task.FromResult(serverUpdater.Restart() ? (IActionResult)Ok() : StatusCode((int)HttpStatusCode.NotImplemented));
|
||||
public Task<IActionResult> Delete() {
|
||||
try
|
||||
{
|
||||
return Task.FromResult(serverUpdater.Restart() ? (IActionResult)Ok() : UnprocessableEntity(new ErrorMessage
|
||||
{
|
||||
Message = RestartNotSupportedException
|
||||
}));
|
||||
}
|
||||
catch (InvalidOperationException)
|
||||
{
|
||||
return Task.FromResult<IActionResult>(StatusCode((int)HttpStatusCode.ServiceUnavailable));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IdentityModel.Tokens.Jwt;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Security.Claims;
|
||||
using System.Threading.Tasks;
|
||||
using Tgstation.Server.Api;
|
||||
@@ -150,6 +151,15 @@ namespace Tgstation.Server.Host.Controllers
|
||||
{
|
||||
ApiHeaders = new ApiHeaders(Request.GetTypedHeaders());
|
||||
|
||||
if(!ApiHeaders.Compatible())
|
||||
{
|
||||
await StatusCode((int)HttpStatusCode.UpgradeRequired, new ErrorMessage
|
||||
{
|
||||
Message = "Provided API version is incompatible with server version!"
|
||||
}).ExecuteResultAsync(context).ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (requireInstance)
|
||||
{
|
||||
if(!ApiHeaders.InstanceId.HasValue)
|
||||
|
||||
@@ -99,7 +99,7 @@ namespace Tgstation.Server.Host.Controllers
|
||||
result.InstallJob = job.ToApi();
|
||||
}
|
||||
result.Version = byondManager.ActiveVersion;
|
||||
return Json(result);
|
||||
return result.InstallJob != null ? (IActionResult)Accepted(result) : Json(result);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -100,14 +100,7 @@ namespace Tgstation.Server.Host.Controllers
|
||||
|
||||
DatabaseContext.ChatBots.Add(dbModel);
|
||||
|
||||
try
|
||||
{
|
||||
await DatabaseContext.Save(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch (DbUpdateException)
|
||||
{
|
||||
return Conflict();
|
||||
}
|
||||
await DatabaseContext.Save(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
try
|
||||
{
|
||||
@@ -132,7 +125,7 @@ namespace Tgstation.Server.Host.Controllers
|
||||
{
|
||||
return BadRequest(new ErrorMessage { Message = e.Message });
|
||||
}
|
||||
return Json(dbModel.ToApi());
|
||||
return StatusCode((int)HttpStatusCode.Created, dbModel.ToApi());
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
||||
@@ -58,7 +58,7 @@ namespace Tgstation.Server.Host.Controllers
|
||||
|
||||
newFile.Content = null;
|
||||
|
||||
return Json(newFile);
|
||||
return model.LastReadHash == null ? (IActionResult)StatusCode((int)HttpStatusCode.Created, newFile) : Json(newFile);
|
||||
}
|
||||
catch(NotImplementedException)
|
||||
{
|
||||
@@ -127,5 +127,26 @@ namespace Tgstation.Server.Host.Controllers
|
||||
/// <inheritdoc />
|
||||
[TgsAuthorize(ConfigurationRights.List)]
|
||||
public override Task<IActionResult> List(CancellationToken cancellationToken) => Directory(null, cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
[TgsAuthorize(ConfigurationRights.Write)]
|
||||
public override async Task<IActionResult> Create([FromBody] ConfigurationFile model, CancellationToken cancellationToken)
|
||||
{
|
||||
if (ForbidDueToModeConflicts())
|
||||
return Forbid();
|
||||
|
||||
try
|
||||
{
|
||||
return await instanceManager.GetInstance(Instance).Configuration.CreateDirectory(model.Path, AuthenticationContext.SystemIdentity, cancellationToken).ConfigureAwait(false) ? (IActionResult)Json(model) : StatusCode((int)HttpStatusCode.Created, model);
|
||||
}
|
||||
catch (NotImplementedException)
|
||||
{
|
||||
return StatusCode((int)HttpStatusCode.NotImplemented);
|
||||
}
|
||||
catch (UnauthorizedAccessException)
|
||||
{
|
||||
return Forbid();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,7 +74,7 @@ namespace Tgstation.Server.Host.Controllers
|
||||
throw new InvalidOperationException("Watchdog already running!");
|
||||
},
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
return Json(job.ToApi());
|
||||
return Accepted(job.ToApi());
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
||||
@@ -93,7 +93,7 @@ namespace Tgstation.Server.Host.Controllers
|
||||
Instance = Instance
|
||||
};
|
||||
await jobManager.RegisterOperation(job, (paramJob, serviceProvider, progressReporter, ct) => RunCompile(paramJob, serviceProvider, Instance, ct), cancellationToken).ConfigureAwait(false);
|
||||
return Json(job.ToApi());
|
||||
return Accepted(job.ToApi());
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
||||
@@ -3,6 +3,7 @@ using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Tgstation.Server.Api;
|
||||
@@ -65,7 +66,10 @@ namespace Tgstation.Server.Host.Controllers
|
||||
/// <returns><see cref="Application.Version"/></returns>
|
||||
[TgsAuthorize]
|
||||
[HttpGet]
|
||||
public JsonResult Home() => Json(new { application.Version });
|
||||
public JsonResult Home() => Json(new Api.Models.ServerInformation {
|
||||
Version = application.Version,
|
||||
ApiVersion = ApiHeaders.Version
|
||||
});
|
||||
|
||||
/// <summary>
|
||||
/// Attempt to authenticate a <see cref="User"/> using <see cref="ApiController.ApiHeaders"/>
|
||||
|
||||
@@ -183,7 +183,7 @@ namespace Tgstation.Server.Host.Controllers
|
||||
|
||||
Logger.LogInformation("{0} created instance {1}: {2}", AuthenticationContext.User.Name, newInstance.Name, newInstance.Id);
|
||||
|
||||
return Json(newInstance.ToApi());
|
||||
return StatusCode((int)HttpStatusCode.Created, newInstance.ToApi());
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
||||
@@ -70,7 +70,7 @@ namespace Tgstation.Server.Host.Controllers
|
||||
DatabaseContext.InstanceUsers.Add(dbUser);
|
||||
|
||||
await DatabaseContext.Save(cancellationToken).ConfigureAwait(false);
|
||||
return Json(dbUser.ToApi());
|
||||
return StatusCode((int)HttpStatusCode.Created, dbUser.ToApi());
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
||||
@@ -174,7 +174,7 @@ namespace Tgstation.Server.Host.Controllers
|
||||
api.IsGitHub = model.Origin.ToUpperInvariant().Contains(uiGitHub);
|
||||
api.ActiveJob = job.ToApi();
|
||||
|
||||
return Json(api);
|
||||
return StatusCode((int)HttpStatusCode.Created, api);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -207,7 +207,7 @@ namespace Tgstation.Server.Host.Controllers
|
||||
var api = currentModel.ToApi();
|
||||
await jobManager.RegisterOperation(job, (paramJob, serviceProvider, progressReporter, ct) => instanceManager.GetInstance(Instance).RepositoryManager.DeleteRepository(cancellationToken), cancellationToken).ConfigureAwait(false);
|
||||
api.ActiveJob = job.ToApi();
|
||||
return Ok();
|
||||
return Accepted(api);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -595,7 +595,7 @@ namespace Tgstation.Server.Host.Controllers
|
||||
}
|
||||
}, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return Json(job.ToApi());
|
||||
return Accepted(job.ToApi());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -118,7 +118,7 @@ namespace Tgstation.Server.Host.Controllers
|
||||
|
||||
await DatabaseContext.Save(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return Json(dbUser.ToApi());
|
||||
return StatusCode((int)HttpStatusCode.Created, dbUser.ToApi());
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
||||
@@ -24,6 +24,14 @@ namespace Tgstation.Server.Host.IO
|
||||
/// <returns>A <see cref="IEnumerable{T}"/> of directory names in <paramref name="path"/></returns>
|
||||
IEnumerable<string> GetDirectories(string path, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Create an empty directory at <paramref name="path"/>
|
||||
/// </summary>
|
||||
/// <param name="path">The path to create</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation. Usage may result in partial writes</param>
|
||||
/// <returns><see langword="true"/> if the directory already existed, <see langword="false"/> otherwise</returns>
|
||||
bool CreateDirectory(string path, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Read the <see cref="byte"/>s of a file at a given <paramref name="path"/>
|
||||
/// </summary>
|
||||
|
||||
@@ -11,6 +11,16 @@ namespace Tgstation.Server.Host.IO
|
||||
/// <inheritdoc />
|
||||
sealed class SynchronousIOManager : ISynchronousIOManager
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public bool CreateDirectory(string path, CancellationToken cancellationToken)
|
||||
{
|
||||
if (IsDirectory(path))
|
||||
return true;
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
Directory.CreateDirectory(path);
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IEnumerable<string> GetDirectories(string path, CancellationToken cancellationToken)
|
||||
{
|
||||
|
||||
@@ -11,16 +11,6 @@ namespace Tgstation.Server.Host.Models
|
||||
/// <inheritdoc />
|
||||
sealed class DatabaseSeeder : IDatabaseSeeder
|
||||
{
|
||||
/// <summary>
|
||||
/// The name of the default admin user
|
||||
/// </summary>
|
||||
const string AdminName = "Admin";
|
||||
|
||||
/// <summary>
|
||||
/// The default admin password
|
||||
/// </summary>
|
||||
const string DefaultAdminPassword = "ISolemlySwearToDeleteTheDataDirectory";
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="ICryptographySuite"/> for the <see cref="DatabaseContext{TParentContext}"/>
|
||||
/// </summary>
|
||||
@@ -43,11 +33,11 @@ namespace Tgstation.Server.Host.Models
|
||||
AdministrationRights = (AdministrationRights)~0U,
|
||||
CreatedAt = DateTimeOffset.Now,
|
||||
InstanceManagerRights = (InstanceManagerRights)~0U,
|
||||
Name = AdminName,
|
||||
CanonicalName = AdminName.ToUpperInvariant(),
|
||||
Name = Api.Models.User.AdminName,
|
||||
CanonicalName = Api.Models.User.AdminName.ToUpperInvariant(),
|
||||
Enabled = true,
|
||||
};
|
||||
cryptographySuite.SetUserPassword(admin, DefaultAdminPassword);
|
||||
cryptographySuite.SetUserPassword(admin, Api.Models.User.DefaultAdminPassword);
|
||||
databaseContext.Users.Add(admin);
|
||||
}
|
||||
|
||||
@@ -61,13 +51,13 @@ namespace Tgstation.Server.Host.Models
|
||||
/// <inheritdoc />
|
||||
public async Task ResetAdminPassword(IDatabaseContext databaseContext, CancellationToken cancellationToken)
|
||||
{
|
||||
var admin = await databaseContext.Users.Where(x => x.CanonicalName == AdminName.ToUpperInvariant()).FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false);
|
||||
var admin = await databaseContext.Users.Where(x => x.CanonicalName == Api.Models.User.AdminName.ToUpperInvariant()).FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false);
|
||||
if (admin == default)
|
||||
SeedAdminUser(databaseContext);
|
||||
else
|
||||
{
|
||||
admin.Enabled = true;
|
||||
cryptographySuite.SetUserPassword(admin, DefaultAdminPassword);
|
||||
cryptographySuite.SetUserPassword(admin, Api.Models.User.DefaultAdminPassword);
|
||||
}
|
||||
|
||||
await databaseContext.Save(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
namespace Tgstation.Server.Host
|
||||
{
|
||||
/// <inheritdoc />
|
||||
sealed class ServerFactory : IServerFactory
|
||||
public sealed class ServerFactory : IServerFactory
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public IServer CreateServer(string[] args, string updatePath) => new Server(WebHost.CreateDefaultBuilder(args), updatePath);
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Threading.Tasks;
|
||||
using Tgstation.Server.Client;
|
||||
|
||||
namespace Tgstation.Server.Tests
|
||||
{
|
||||
sealed class AdministrationTest
|
||||
{
|
||||
readonly IAdministrationClient client;
|
||||
|
||||
public AdministrationTest(IAdministrationClient client)
|
||||
{
|
||||
this.client = client ?? throw new ArgumentNullException(nameof(client));
|
||||
}
|
||||
|
||||
public async Task Run()
|
||||
{
|
||||
await TestRead().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
async Task TestRead()
|
||||
{
|
||||
var model = await client.Read(default).ConfigureAwait(false);
|
||||
Assert.AreEqual(RuntimeInformation.IsOSPlatform(OSPlatform.Windows), model.WindowsHost);
|
||||
|
||||
//uhh not much else to do
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
using System;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Reflection;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Tgstation.Server.Api;
|
||||
using Tgstation.Server.Api.Models;
|
||||
using Tgstation.Server.Client;
|
||||
using Tgstation.Server.Host;
|
||||
|
||||
namespace Tgstation.Server.Tests
|
||||
{
|
||||
[TestClass]
|
||||
public sealed class IntegrationTest
|
||||
{
|
||||
readonly IServerClientFactory clientFactory = new ServerClientFactory(new ProductHeaderValue(Assembly.GetExecutingAssembly().GetName().Name, Assembly.GetExecutingAssembly().GetName().Version.ToString()));
|
||||
|
||||
[TestMethod]
|
||||
public async Task FullMonty()
|
||||
{
|
||||
using (var server = new TestingServer())
|
||||
using (var serverCts = new CancellationTokenSource())
|
||||
{
|
||||
var serverTask = server.RunAsync(serverCts.Token);
|
||||
try
|
||||
{
|
||||
using (var adminClient = await clientFactory.CreateServerClient(server.Url, User.AdminName, User.DefaultAdminPassword).ConfigureAwait(false))
|
||||
{
|
||||
var serverInfo = await adminClient.Version(default).ConfigureAwait(false);
|
||||
|
||||
Assert.AreEqual(ApiHeaders.Version, serverInfo.ApiVersion);
|
||||
Assert.AreEqual(typeof(IServer).Assembly.GetName().Version, serverInfo.Version);
|
||||
|
||||
await new AdministrationTest(adminClient.Administration).Run().ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
serverCts.Cancel();
|
||||
try
|
||||
{
|
||||
await serverTask.ConfigureAwait(false);
|
||||
}
|
||||
catch (OperationCanceledException) { }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Tgstation.Server.Host;
|
||||
|
||||
namespace Tgstation.Server.Tests
|
||||
{
|
||||
sealed class TestingServer : IServer
|
||||
{
|
||||
public Uri Url { get; }
|
||||
public bool RestartRequested => realServer.RestartRequested;
|
||||
|
||||
readonly IServer realServer;
|
||||
readonly string databasePath;
|
||||
|
||||
public TestingServer()
|
||||
{
|
||||
databasePath = Path.GetTempFileName();
|
||||
File.Delete(databasePath);
|
||||
Url = new Uri("http://localhost:5001");
|
||||
realServer = new ServerFactory().CreateServer(new string[]
|
||||
{
|
||||
"--urls",
|
||||
Url.ToString(),
|
||||
"Database:DatabaseType=Sqlite",
|
||||
String.Format(CultureInfo.InvariantCulture, "Database:ConnectionString=Data Source={0}", databasePath)
|
||||
,"Database:NoMigrations=true" //TODO: remove this when migrations are added
|
||||
}, null);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
realServer.Dispose();
|
||||
File.Delete(databasePath);
|
||||
}
|
||||
|
||||
public Task RunAsync(CancellationToken cancellationToken) => realServer.RunAsync(cancellationToken);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netcoreapp2.0</TargetFramework>
|
||||
|
||||
<IsPackable>false</IsPackable>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
|
||||
<LangVersion>7.1</LangVersion>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
|
||||
<LangVersion>7.1</LangVersion>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.7.0" />
|
||||
<PackageReference Include="MSTest.TestAdapter" Version="1.2.1" />
|
||||
<PackageReference Include="MSTest.TestFramework" Version="1.2.1" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\Tgstation.Server.Client\Tgstation.Server.Client.csproj" />
|
||||
<ProjectReference Include="..\..\src\Tgstation.Server.Host\Tgstation.Server.Host.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
+7
-26
@@ -101,8 +101,8 @@ EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "docs", "docs", "{DCC75431-7913-4306-9FAB-70998D440BCE}"
|
||||
ProjectSection(SolutionItems) = preProject
|
||||
docs\API.dox = docs\API.dox
|
||||
docs\ArchitectureOverview.png = docs\ArchitectureOverview.png
|
||||
docs\Architecture.dox = docs\Architecture.dox
|
||||
docs\ArchitectureOverview.png = docs\ArchitectureOverview.png
|
||||
EndProjectSection
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".github", ".github", "{E82104F4-F5C4-4786-ACD4-B635166CDB21}"
|
||||
@@ -112,85 +112,66 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".github", ".github", "{E821
|
||||
.github\ISSUE_TEMPLATE.md = .github\ISSUE_TEMPLATE.md
|
||||
EndProjectSection
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tgstation.Server.Tests", "tests\Tgstation.Server.Tests\Tgstation.Server.Tests.csproj", "{09056964-1C74-445A-96EC-33F6DFC07916}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
Docker|Any CPU = Docker|Any CPU
|
||||
Release|Any CPU = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{A09C947F-4B9B-4AEE-AB50-47055EA05F18}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{A09C947F-4B9B-4AEE-AB50-47055EA05F18}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{A09C947F-4B9B-4AEE-AB50-47055EA05F18}.Docker|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{A09C947F-4B9B-4AEE-AB50-47055EA05F18}.Docker|Any CPU.Build.0 = Release|Any CPU
|
||||
{A09C947F-4B9B-4AEE-AB50-47055EA05F18}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{A09C947F-4B9B-4AEE-AB50-47055EA05F18}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{D36B99C4-E771-42D6-A95F-1102B3E236DF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{D36B99C4-E771-42D6-A95F-1102B3E236DF}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{D36B99C4-E771-42D6-A95F-1102B3E236DF}.Docker|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{D36B99C4-E771-42D6-A95F-1102B3E236DF}.Docker|Any CPU.Build.0 = Release|Any CPU
|
||||
{D36B99C4-E771-42D6-A95F-1102B3E236DF}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{D36B99C4-E771-42D6-A95F-1102B3E236DF}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{2B69AD6D-2B5A-4023-8EAD-0BD1B18E028A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{2B69AD6D-2B5A-4023-8EAD-0BD1B18E028A}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{2B69AD6D-2B5A-4023-8EAD-0BD1B18E028A}.Docker|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{2B69AD6D-2B5A-4023-8EAD-0BD1B18E028A}.Docker|Any CPU.Build.0 = Release|Any CPU
|
||||
{2B69AD6D-2B5A-4023-8EAD-0BD1B18E028A}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{2B69AD6D-2B5A-4023-8EAD-0BD1B18E028A}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{8B4A208D-A48A-4A5D-8B94-E2661138865D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{8B4A208D-A48A-4A5D-8B94-E2661138865D}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{8B4A208D-A48A-4A5D-8B94-E2661138865D}.Docker|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{8B4A208D-A48A-4A5D-8B94-E2661138865D}.Docker|Any CPU.Build.0 = Release|Any CPU
|
||||
{8B4A208D-A48A-4A5D-8B94-E2661138865D}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{8B4A208D-A48A-4A5D-8B94-E2661138865D}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{29927416-3B78-49A7-A560-5CCAA638B6B4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{29927416-3B78-49A7-A560-5CCAA638B6B4}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{29927416-3B78-49A7-A560-5CCAA638B6B4}.Docker|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{29927416-3B78-49A7-A560-5CCAA638B6B4}.Docker|Any CPU.Build.0 = Release|Any CPU
|
||||
{29927416-3B78-49A7-A560-5CCAA638B6B4}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{29927416-3B78-49A7-A560-5CCAA638B6B4}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{E0AC911F-7675-4A91-9499-D8A2E2390AAD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{E0AC911F-7675-4A91-9499-D8A2E2390AAD}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{E0AC911F-7675-4A91-9499-D8A2E2390AAD}.Docker|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{E0AC911F-7675-4A91-9499-D8A2E2390AAD}.Docker|Any CPU.Build.0 = Release|Any CPU
|
||||
{E0AC911F-7675-4A91-9499-D8A2E2390AAD}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{E0AC911F-7675-4A91-9499-D8A2E2390AAD}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{BE0E8F49-334F-49D7-A04D-D82E9AB7AA36}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{BE0E8F49-334F-49D7-A04D-D82E9AB7AA36}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{BE0E8F49-334F-49D7-A04D-D82E9AB7AA36}.Docker|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{BE0E8F49-334F-49D7-A04D-D82E9AB7AA36}.Docker|Any CPU.Build.0 = Release|Any CPU
|
||||
{BE0E8F49-334F-49D7-A04D-D82E9AB7AA36}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{BE0E8F49-334F-49D7-A04D-D82E9AB7AA36}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{E5301AF1-4F74-4982-BF24-95F23CC5D5B2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{E5301AF1-4F74-4982-BF24-95F23CC5D5B2}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{E5301AF1-4F74-4982-BF24-95F23CC5D5B2}.Docker|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{E5301AF1-4F74-4982-BF24-95F23CC5D5B2}.Docker|Any CPU.Build.0 = Release|Any CPU
|
||||
{E5301AF1-4F74-4982-BF24-95F23CC5D5B2}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{E5301AF1-4F74-4982-BF24-95F23CC5D5B2}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{A3362FF6-550F-480F-859E-8EC1EB6EAB31}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{A3362FF6-550F-480F-859E-8EC1EB6EAB31}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{A3362FF6-550F-480F-859E-8EC1EB6EAB31}.Docker|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{A3362FF6-550F-480F-859E-8EC1EB6EAB31}.Docker|Any CPU.Build.0 = Release|Any CPU
|
||||
{A3362FF6-550F-480F-859E-8EC1EB6EAB31}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{A3362FF6-550F-480F-859E-8EC1EB6EAB31}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{5D2D682C-6BF0-439C-850B-6AB945BBEAEA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{5D2D682C-6BF0-439C-850B-6AB945BBEAEA}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{5D2D682C-6BF0-439C-850B-6AB945BBEAEA}.Docker|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{5D2D682C-6BF0-439C-850B-6AB945BBEAEA}.Docker|Any CPU.Build.0 = Release|Any CPU
|
||||
{5D2D682C-6BF0-439C-850B-6AB945BBEAEA}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{5D2D682C-6BF0-439C-850B-6AB945BBEAEA}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{7500F776-4384-4B5F-A8D8-22461CAD108B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{7500F776-4384-4B5F-A8D8-22461CAD108B}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{7500F776-4384-4B5F-A8D8-22461CAD108B}.Docker|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{7500F776-4384-4B5F-A8D8-22461CAD108B}.Docker|Any CPU.Build.0 = Release|Any CPU
|
||||
{7500F776-4384-4B5F-A8D8-22461CAD108B}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{7500F776-4384-4B5F-A8D8-22461CAD108B}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{AA80A190-52E2-4BE3-BFEB-1F148D9E9007}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{AA80A190-52E2-4BE3-BFEB-1F148D9E9007}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{AA80A190-52E2-4BE3-BFEB-1F148D9E9007}.Docker|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{AA80A190-52E2-4BE3-BFEB-1F148D9E9007}.Docker|Any CPU.Build.0 = Release|Any CPU
|
||||
{AA80A190-52E2-4BE3-BFEB-1F148D9E9007}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{AA80A190-52E2-4BE3-BFEB-1F148D9E9007}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{09056964-1C74-445A-96EC-33F6DFC07916}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{09056964-1C74-445A-96EC-33F6DFC07916}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{09056964-1C74-445A-96EC-33F6DFC07916}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{09056964-1C74-445A-96EC-33F6DFC07916}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"info": {
|
||||
"_postman_id": "b7e61e38-6891-473e-8ab4-869e5cf41a96",
|
||||
"_postman_id": "bb44d5ca-de20-4da3-880d-1ceacd54fe4a",
|
||||
"name": "TGS",
|
||||
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
|
||||
},
|
||||
@@ -2624,6 +2624,49 @@
|
||||
}
|
||||
},
|
||||
"response": []
|
||||
},
|
||||
{
|
||||
"name": "Create NewDir",
|
||||
"request": {
|
||||
"method": "PUT",
|
||||
"header": [
|
||||
{
|
||||
"key": "Accept",
|
||||
"value": "application/json"
|
||||
},
|
||||
{
|
||||
"key": "User-Agent",
|
||||
"value": "Postman/1.0"
|
||||
},
|
||||
{
|
||||
"key": "Api",
|
||||
"value": "Tgstation.Server.Api/4.0.0.0"
|
||||
},
|
||||
{
|
||||
"key": "Content-Type",
|
||||
"value": "application/json"
|
||||
},
|
||||
{
|
||||
"key": "Instance",
|
||||
"value": "1"
|
||||
}
|
||||
],
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n\t\"path\": \"NewDir\"\n}"
|
||||
},
|
||||
"url": {
|
||||
"raw": "localhost:5000/Config",
|
||||
"host": [
|
||||
"localhost"
|
||||
],
|
||||
"port": "5000",
|
||||
"path": [
|
||||
"Config"
|
||||
]
|
||||
}
|
||||
},
|
||||
"response": []
|
||||
}
|
||||
],
|
||||
"_postman_isSubFolder": true
|
||||
|
||||
@@ -3,4 +3,6 @@ Verify the byond cache folder location on linux
|
||||
Test watchdog
|
||||
|
||||
Only show user name and ID when serializing to API
|
||||
In fact remove IApiConvertable<> altogether, it's not required by anything
|
||||
In fact remove IApiConvertable<> altogether, it's not required by anything
|
||||
|
||||
Chat channel tagging
|
||||
|
||||
Reference in New Issue
Block a user