diff --git a/.dockerignore b/.dockerignore
index c3189160e5..341bd88cf1 100644
--- a/.dockerignore
+++ b/.dockerignore
@@ -5,5 +5,7 @@ docker-compose.yml
.vs
.vscode
legacy
+packages
+build
*/bin
*/obj
diff --git a/.gitignore b/.gitignore
index a0fa5f96e2..9b60069c91 100644
--- a/.gitignore
+++ b/.gitignore
@@ -11,3 +11,4 @@ artifacts/
*DS_Store
*.sln.ide
/TestResults
+/src/Tgstation.Server.Host/appsettings.Development.json
diff --git a/build/Dockerfile b/build/Dockerfile
index 5332fa7e91..2b1cb8c47d 100644
--- a/build/Dockerfile
+++ b/build/Dockerfile
@@ -1,4 +1,4 @@
-FROM microsoft/dotnet:2.1-sdk AS build
+FROM microsoft/dotnet:2.0-sdk AS build
WORKDIR /src
COPY tgstation-server.sln ./
@@ -36,8 +36,14 @@ RUN dotnet test -c Release
FROM build AS publish
RUN dotnet publish -c Release -o /app
-FROM microsoft/dotnet:2.1-runtime
+FROM microsoft/dotnet:2.0-runtime
EXPOSE 80
+
WORKDIR /app
+
COPY --from=publish /app .
+
+RUN mkdir /config_data && mv appsettings.Docker.json /config_data/appsettings.Production.json
+VOLUME ["/config_data"]
+
ENTRYPOINT ["dotnet", "Tgstation.Server.Host.Console.dll"]
diff --git a/src/Tgstation.Server.Api/ApiHeaders.cs b/src/Tgstation.Server.Api/ApiHeaders.cs
new file mode 100644
index 0000000000..5db3493ae5
--- /dev/null
+++ b/src/Tgstation.Server.Api/ApiHeaders.cs
@@ -0,0 +1,222 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Linq;
+using System.Net.Http.Headers;
+using System.Reflection;
+using Microsoft.AspNetCore.Http.Headers;
+using Microsoft.Net.Http.Headers;
+using Microsoft.Extensions.Primitives;
+
+namespace Tgstation.Server.Api
+{
+ ///
+ /// Represents the header that must be present for every server request
+ ///
+ public sealed class ApiHeaders
+ {
+ ///
+ /// TODO: Remove this when https://github.com/dotnet/corefx/pull/26701 makes it into the sdk
+ ///
+ public const string ApplicationJson = "application/json";
+
+ ///
+ /// The header key
+ ///
+ const string usernameHeader = "Username";
+
+ ///
+ /// The header key
+ ///
+ const string instanceIdHeader = "InstanceId";
+
+ ///
+ /// The JWT authentication header scheme
+ ///
+ const string jwtAuthenticationScheme = "Bearer";
+
+ ///
+ /// The password authentication header scheme
+ ///
+ const string passwordAuthenticationScheme = "Password";
+
+ ///
+ /// The current
+ ///
+ static readonly AssemblyName assemblyName = Assembly.GetExecutingAssembly().GetName();
+
+ ///
+ /// The being accessed
+ ///
+ public long? InstanceId { get; set; }
+
+ ///
+ /// The client's user agent
+ ///
+ public ProductHeaderValue UserAgent { get; }
+
+ ///
+ /// The client's API version
+ ///
+ public Version ApiVersion { get; }
+
+ ///
+ /// The client's JWT
+ ///
+ public string Token { get; }
+
+ ///
+ /// The client's username
+ ///
+ public string Username { get; }
+
+ ///
+ /// The client's password
+ ///
+ public string Password { get; }
+
+ ///
+ /// If the header uses password or JWT authentication
+ ///
+ public bool IsTokenAuthentication => Token != null;
+
+ ///
+ /// Construct for JWT authentication
+ ///
+ /// The value of
+ /// The value of
+ public ApiHeaders(ProductHeaderValue userAgent, string token) : this(userAgent, token, null, null)
+ {
+ if (userAgent == null)
+ throw new ArgumentNullException(nameof(userAgent));
+ if (token == null)
+ throw new ArgumentNullException(nameof(token));
+ }
+
+ ///
+ /// Construct for password authentication
+ ///
+ /// The value of
+ /// The value of
+ /// The value of
+ public ApiHeaders(ProductHeaderValue userAgent, string username, string password) : this(userAgent, null, username, password)
+ {
+ if (userAgent == null)
+ throw new ArgumentNullException(nameof(userAgent));
+ if (username == null)
+ throw new ArgumentNullException(nameof(username));
+ if (password == null)
+ throw new ArgumentNullException(nameof(password));
+ }
+
+ ///
+ /// Construct and validates from a set of
+ ///
+ /// The containing the
+ public ApiHeaders(RequestHeaders requestHeaders)
+ {
+ var jsonAccept = new Microsoft.Net.Http.Headers.MediaTypeHeaderValue(ApplicationJson);
+ if (!requestHeaders.Accept.Any(x => x.MediaType == jsonAccept.MediaType))
+ throw new InvalidOperationException(String.Format(CultureInfo.InvariantCulture, "Client does not accept {0}!", ApplicationJson));
+
+ if (!requestHeaders.Headers.TryGetValue(HeaderNames.UserAgent, out StringValues userAgentValues) || !ProductInfoHeaderValue.TryParse(userAgentValues.FirstOrDefault(), out ProductInfoHeaderValue clientUserAgent))
+ throw new InvalidOperationException(String.Format(CultureInfo.InvariantCulture, "Missing {0} headers!", HeaderNames.UserAgent));
+
+ //assure the client user agent has a name and version
+ if (String.IsNullOrWhiteSpace(clientUserAgent.Product.Name) || !Version.TryParse(clientUserAgent.Product.Version, out Version clientVersion))
+ throw new InvalidOperationException("Malformed client user agent!");
+
+ var apiUserAgentHeader = userAgentValues[1];
+ //make sure the api header matches ours
+ if (!ProductInfoHeaderValue.TryParse(apiUserAgentHeader, out ProductInfoHeaderValue apiUserAgent) || apiUserAgent.Product.Name != assemblyName.Name)
+ throw new InvalidOperationException("Missing API user agent!");
+
+ if (!Version.TryParse(apiUserAgent.Product.Version, out Version ApiVersion))
+ throw new InvalidOperationException("Malformed API version!");
+
+ //check api version compatibility
+ var ourVersion = assemblyName.Version;
+ if (ourVersion.Major != ApiVersion.Major || ourVersion.Minor != ApiVersion.Minor || ourVersion.Build > ApiVersion.Build)
+ throw new InvalidOperationException(String.Format(CultureInfo.InvariantCulture, "Given API version is incompatible with version {0}!", ourVersion));
+
+ 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();
+ var splits = new List(auth.Split(' '));
+ var scheme = splits.First();
+ if (String.IsNullOrWhiteSpace(scheme))
+ throw new InvalidOperationException("Missing authentication scheme!");
+
+ splits.RemoveAt(0);
+ var parameter = String.Concat(splits);
+ if (String.IsNullOrEmpty(parameter))
+ throw new InvalidOperationException("Missing authentication parameter!");
+
+ if(requestHeaders.Headers.TryGetValue(instanceIdHeader, out StringValues instanceIdValues))
+ {
+ var instanceIdString = instanceIdValues.FirstOrDefault();
+ if (instanceIdString != default && Int64.TryParse(instanceIdString, out long instanceId))
+ InstanceId = instanceId;
+ }
+
+ switch (scheme)
+ {
+ case jwtAuthenticationScheme:
+ Token = parameter;
+ break;
+ case passwordAuthenticationScheme:
+ Password = parameter;
+ var fail = !requestHeaders.Headers.TryGetValue(usernameHeader, out StringValues values);
+ if (!fail)
+ {
+ Username = values.FirstOrDefault();
+ fail = String.IsNullOrWhiteSpace(Username);
+ }
+ if (fail)
+ throw new InvalidOperationException("Missing Username header!");
+ break;
+ default:
+ throw new InvalidOperationException("Invalid authentication scheme!");
+ }
+ }
+
+ ///
+ /// Construct
+ ///
+ /// The value of
+ /// The value of
+ /// The value of
+ /// The value of
+ ApiHeaders(ProductHeaderValue userAgent, string token, string username, string password)
+ {
+ UserAgent = userAgent;
+ Token = token;
+ Username = username;
+ Password = password;
+ }
+
+ ///
+ /// Set using the . This initially clears
+ ///
+ /// The to set
+ public void SetRequestHeaders(HttpRequestHeaders headers)
+ {
+ if (headers == null)
+ throw new ArgumentNullException(nameof(headers));
+
+ headers.Clear();
+ headers.Accept.Add(new MediaTypeWithQualityHeaderValue(ApplicationJson));
+ if (IsTokenAuthentication)
+ headers.Authorization = new AuthenticationHeaderValue(jwtAuthenticationScheme, Token);
+ else
+ {
+ headers.Authorization = new AuthenticationHeaderValue(passwordAuthenticationScheme, Password);
+ headers.Add(usernameHeader, Username);
+ }
+ headers.UserAgent.Add(new ProductInfoHeaderValue(UserAgent));
+ headers.UserAgent.Add(new ProductInfoHeaderValue(new ProductHeaderValue(assemblyName.Name, assemblyName.Version.ToString())));
+ if(InstanceId.HasValue)
+ headers.Add(instanceIdHeader, InstanceId.ToString());
+ }
+ }
+}
diff --git a/src/Tgstation.Server.Api/ModelAttribute.cs b/src/Tgstation.Server.Api/ModelAttribute.cs
index 6145721634..df50284cf3 100644
--- a/src/Tgstation.Server.Api/ModelAttribute.cs
+++ b/src/Tgstation.Server.Api/ModelAttribute.cs
@@ -6,7 +6,7 @@ namespace Tgstation.Server.Api
///
/// Indicates the rights for a model
///
- [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)]
+ [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = true)]
public sealed class ModelAttribute : Attribute
{
///
diff --git a/src/Tgstation.Server.Api/Models/Administration.cs b/src/Tgstation.Server.Api/Models/Administration.cs
index 7fd8bdfc84..7f04e2d428 100644
--- a/src/Tgstation.Server.Api/Models/Administration.cs
+++ b/src/Tgstation.Server.Api/Models/Administration.cs
@@ -4,36 +4,15 @@ using Tgstation.Server.Api.Rights;
namespace Tgstation.Server.Api.Models
{
- ///
- /// Metadata about an installation
- ///
- [Model(RightsType.Administration)]
- public sealed class Administration
+ ///
+ public sealed class Administration : Internal.ServerSettings
{
- ///
- /// Use the specified Windows/UNIX authentication to authorize users. Setting this to enables full administrative anonymous access
- ///
- [Permissions(ReadRight = AdministrationRights.ChangeAuthenticationGroup, WriteRight = AdministrationRights.ChangeAuthenticationGroup)]
- public string SystemAuthenticationGroup { get; set; }
-
- ///
- /// Automatically send unhandled exception data to a public collection service. This will be limited to system information, path data, and game code compilation information.
- ///
- [Permissions(ReadRight = AdministrationRights.ChangeTelemetry, WriteRight = AdministrationRights.ChangeTelemetry)]
- public bool EnableTelemetry { get; set; }
-
///
/// If the instances will not be stopped when the server exits. Resets to when the server restarts
///
[Permissions(ReadRight = AdministrationRights.SoftStop, WriteRight = AdministrationRights.SoftStop)]
public bool SoftStop { get; set; }
-
- ///
- /// The git repository to recieve updates to Tgstation.Server.Host from, must include credentials if necessary. If set to upstream pulls will be disabled entirely
- ///
- [Permissions(ReadRight = AdministrationRights.SetUpstreamRepository, WriteRight = AdministrationRights.SetUpstreamRepository)]
- public string UpstreamRepository { get; set; }
-
+
///
/// The latest available version of the Tgstation.Server.Host assembly from the upstream repository. If is higher than 's the update cannot be applied due to API changes
///
@@ -47,7 +26,7 @@ namespace Tgstation.Server.Api.Models
public Version CurrentVersion { get; set; }
///
- /// Users in the
+ /// Users in the
///
[Permissions(DenyWrite = true)]
public IReadOnlyList Users { get; set; }
diff --git a/src/Tgstation.Server.Api/Models/Chat.cs b/src/Tgstation.Server.Api/Models/Chat.cs
deleted file mode 100644
index 543eae1230..0000000000
--- a/src/Tgstation.Server.Api/Models/Chat.cs
+++ /dev/null
@@ -1,71 +0,0 @@
-using System.Collections.Generic;
-using Tgstation.Server.Api.Rights;
-
-namespace Tgstation.Server.Api.Models
-{
- ///
- /// Manage the server chat bots
- ///
- [Model(RightsType.Chat, RequiresInstance = true)]
- public sealed class Chat
- {
- ///
- /// If the IRC client is enabled
- ///
- [Permissions(WriteRight = ChatRights.SetIrcEnabled)]
- public bool IrcEnabled { get; set; }
-
- ///
- /// The IRC server name
- ///
- [Permissions(ReadRight = ChatRights.SetIrcSettings, WriteRight = ChatRights.SetIrcSettings)]
- public string IrcHost { get; set; }
-
- ///
- /// The IRC server port
- ///
- [Permissions(ReadRight = ChatRights.SetIrcSettings, WriteRight = ChatRights.SetIrcSettings)]
- public ushort IrcPort { get; set; }
-
- ///
- /// The IRC server NickServ password
- ///
- [Permissions(ReadRight = ChatRights.SetIrcSettings, WriteRight = ChatRights.SetIrcSettings)]
- public string IrcNickServPassword { get; set; }
-
- ///
- /// Channels the IRC client should join/listen/announce in and allow admin commands
- ///
- [Permissions(WriteRight = ChatRights.SetIrcChannels)]
- public List IrcAdminChannels { get; set; }
-
- ///
- /// Channels the IRC client should join/listen/announce in
- ///
- [Permissions(WriteRight = ChatRights.SetIrcChannels)]
- public List IrcGeneralChannels { get; set; }
-
- ///
- /// If the Discord bot is enabled
- ///
- [Permissions(WriteRight = ChatRights.SetDiscordEnabled)]
- public bool DiscordEnabled { get; set; }
-
- ///
- /// The Discord bot token
- ///
- [Permissions(ReadRight = ChatRights.SetDiscordSettings, WriteRight = ChatRights.SetDiscordSettings)]
- public string DiscordBotToken { get; set; }
-
- ///
- /// Channels the Discord bot should listen/announce in and allow admin commands
- ///
- [Permissions(WriteRight = ChatRights.SetDiscordChannels)]
- public List DiscordAdminChannels { get; set; }
- ///
- /// Channels the Discord bot should listen/announce in
- ///
- [Permissions(WriteRight = ChatRights.SetDiscordChannels)]
- public List DiscordGeneralChannels { get; set; }
- }
-}
diff --git a/src/Tgstation.Server.Api/Models/ChatChannel.cs b/src/Tgstation.Server.Api/Models/ChatChannel.cs
new file mode 100644
index 0000000000..a927090029
--- /dev/null
+++ b/src/Tgstation.Server.Api/Models/ChatChannel.cs
@@ -0,0 +1,23 @@
+namespace Tgstation.Server.Api.Models
+{
+ ///
+ /// Indicates a chat channel
+ ///
+ public class ChatChannel
+ {
+ ///
+ /// The IRC channel name
+ ///
+ public string IrcChannel { get; set; }
+
+ ///
+ /// The Discord channel ID
+ ///
+ public long DiscordChannelId { get; set; }
+
+ ///
+ /// If the is an admin channel
+ ///
+ public bool IsAdminChannel { get; set; }
+ }
+}
diff --git a/src/Tgstation.Server.Api/Models/ChatSettings.cs b/src/Tgstation.Server.Api/Models/ChatSettings.cs
new file mode 100644
index 0000000000..a4cb033170
--- /dev/null
+++ b/src/Tgstation.Server.Api/Models/ChatSettings.cs
@@ -0,0 +1,15 @@
+using System.Collections.Generic;
+using Tgstation.Server.Api.Rights;
+
+namespace Tgstation.Server.Api.Models
+{
+ ///
+ public sealed class ChatSettings : Internal.ChatSettings
+ {
+ ///
+ /// Channels the Discord bot should listen/announce in
+ ///
+ [Permissions(WriteRight = ChatSettingsRights.SetChannels)]
+ public List Channels { get; set; }
+ }
+}
diff --git a/src/Tgstation.Server.Api/Models/CompileJob.cs b/src/Tgstation.Server.Api/Models/CompileJob.cs
new file mode 100644
index 0000000000..2cc0f74988
--- /dev/null
+++ b/src/Tgstation.Server.Api/Models/CompileJob.cs
@@ -0,0 +1,16 @@
+namespace Tgstation.Server.Api.Models
+{
+ ///
+ public sealed class CompileJob : Internal.CompileJob
+ {
+ ///
+ /// The that triggered the job
+ ///
+ public User TriggeredBy { get; set; }
+
+ ///
+ /// Git revision the compiler ran on. Not modifiable
+ ///
+ public RevisionInformation RevisionInformation { get; set; }
+ }
+}
diff --git a/src/Tgstation.Server.Api/Models/Configuration.cs b/src/Tgstation.Server.Api/Models/Configuration.cs
index 4b750c9e6a..16b57be176 100644
--- a/src/Tgstation.Server.Api/Models/Configuration.cs
+++ b/src/Tgstation.Server.Api/Models/Configuration.cs
@@ -30,9 +30,7 @@ namespace Tgstation.Server.Api.Models
///
/// The content of the file. Will be if is or during listing operations
///
-#pragma warning disable CA1819 // Properties should not return arrays
public byte[] Content { get; set; }
-#pragma warning restore CA1819 // Properties should not return arrays
///
/// The MD5 hash of the file when last read by the user. Will be if is . If this doesn't match during update actions, the write will be denied with error code 409
diff --git a/src/Tgstation.Server.Api/Models/DreamDaemon.cs b/src/Tgstation.Server.Api/Models/DreamDaemon.cs
index 652256ef5b..49571a3fa3 100644
--- a/src/Tgstation.Server.Api/Models/DreamDaemon.cs
+++ b/src/Tgstation.Server.Api/Models/DreamDaemon.cs
@@ -1,5 +1,4 @@
-using System;
-using System.Collections.Generic;
+using Tgstation.Server.Api.Models.Internal;
using Tgstation.Server.Api.Rights;
namespace Tgstation.Server.Api.Models
@@ -7,38 +6,13 @@ namespace Tgstation.Server.Api.Models
///
/// Represents an instance of BYOND's DreamDaemon game server. Create action starts the server. Delete action shuts down the server
///
- [Model(RightsType.DreamDaemon, CanCrud = true, RequiresInstance = true)]
- public sealed class DreamDaemon
+ public sealed class DreamDaemon : DreamDaemonSettings
{
- ///
- /// The pull requests merged on the live game version
- ///
- [Permissions(DenyWrite = true, ReadRight = DreamDaemonRights.ReadRevision)]
- public IReadOnlyDictionary PullRequests { get; set; }
-
- ///
- /// The git sha the live game version was compiled at
- ///
- [Permissions(DenyWrite = true, ReadRight = DreamDaemonRights.ReadRevision)]
- public string Sha { get; set; }
-
- ///
- /// The git sha of the origin branch the live game version was compiled at
- ///
- [Permissions(DenyWrite = true, ReadRight = DreamDaemonRights.ReadRevision)]
- public string OriginSha { get; set; }
-
///
/// When the live revision was compiled
///
[Permissions(DenyWrite = true, ReadRight = DreamDaemonRights.ReadRevision)]
- public DateTimeOffset CompiledAt { get; set; }
-
- ///
- /// If starts when it's starts
- ///
- [Permissions(ReadRight = DreamDaemonRights.ReadMetadata, WriteRight = DreamDaemonRights.SetAutoStart)]
- public bool? AutoStart { get; set; }
+ public CompileJob CompileJob { get; set; }
///
/// The current status of
@@ -47,45 +21,15 @@ namespace Tgstation.Server.Api.Models
public DreamDaemonStatus? Status { get; set; }
///
- /// If the BYOND web client can be used to connect to the game server
+ /// The current of
///
- [Permissions(ReadRight = DreamDaemonRights.ReadMetadata, WriteRight = DreamDaemonRights.SetWebClient)]
- public bool? AllowWebClient { get; set; }
-
- ///
- /// If the server is undergoing a soft reset. This may be automatically set by changes to other fields
- ///
- [Permissions(ReadRight = DreamDaemonRights.ReadMetadata, WriteRight = DreamDaemonRights.SoftRestart)]
- public bool? SoftRestart { get; set; }
-
- ///
- /// If the server is undergoing a soft shutdown
- ///
- [Permissions(ReadRight = DreamDaemonRights.ReadMetadata, WriteRight = DreamDaemonRights.SoftShutdown)]
- public bool? SoftShutdown { get; set; }
-
- ///
- /// The level of
- ///
- [Permissions(ReadRight = DreamDaemonRights.ReadMetadata, WriteRight = DreamDaemonRights.SetSecurity)]
- public DreamDaemonSecurity? SecurirtyLevel { get; set; }
-
- ///
- /// The first port uses. This should be the publically advertised port
- ///
- [Permissions(ReadRight = DreamDaemonRights.ReadMetadata, WriteRight = DreamDaemonRights.SetPorts)]
- public ushort? PrimaryPort { get; set; }
-
- ///
- /// The second port uses
- ///
- [Permissions(ReadRight = DreamDaemonRights.ReadMetadata, WriteRight = DreamDaemonRights.SetPorts)]
- public ushort? SecondaryPort { get; set; }
+ [Permissions(DenyWrite = true, ReadRight = DreamDaemonRights.ReadMetadata)]
+ public DreamDaemonSecurity? CurrentSecurity { get; set; }
///
/// The port the running instance is set to
///
[Permissions(DenyWrite = true, ReadRight = DreamDaemonRights.ReadMetadata)]
- public bool CurrentPort { get; set; }
+ public ushort? CurrentPort { get; set; }
}
}
diff --git a/src/Tgstation.Server.Api/Models/DreamDaemonSecurity.cs b/src/Tgstation.Server.Api/Models/DreamDaemonSecurity.cs
index fef6a8f20c..094332d1e4 100644
--- a/src/Tgstation.Server.Api/Models/DreamDaemonSecurity.cs
+++ b/src/Tgstation.Server.Api/Models/DreamDaemonSecurity.cs
@@ -8,7 +8,7 @@
///
/// Server is unrestricted in terms of file access and shell commands
///
- Trusted = 0,
+ Trusted,
///
/// Server will not be able to run shell commands or access files outside it's working directory
///
diff --git a/src/Tgstation.Server.Api/Models/DreamMaker.cs b/src/Tgstation.Server.Api/Models/DreamMaker.cs
index e8a093f5a6..de1d893846 100644
--- a/src/Tgstation.Server.Api/Models/DreamMaker.cs
+++ b/src/Tgstation.Server.Api/Models/DreamMaker.cs
@@ -1,4 +1,4 @@
-using System;
+using Tgstation.Server.Api.Models.Internal;
using Tgstation.Server.Api.Rights;
namespace Tgstation.Server.Api.Models
@@ -6,60 +6,18 @@ namespace Tgstation.Server.Api.Models
///
/// Represents the state of the DreamMaker compiler. Create action starts a new compile. Delete action cancels the current compile
///
- [Model(RightsType.DreamMaker, ReadRight = DreamMakerRights.Read, CanCrud = true, RequiresInstance = true)]
- public sealed class DreamMaker
+ public sealed class DreamMaker : DreamMakerSettings
{
///
- /// When the compilation started
+ /// The last ran
///
[Permissions(DenyWrite = true)]
- public DateTimeOffset StartedAt { get; set; }
- ///
- /// When the compilation finished
- ///
- [Permissions(DenyWrite = true)]
- public DateTimeOffset FinishedAt { get; set; }
+ public CompileJob LastJob { get; set; }
+
///
/// The of the compiler
///
[Permissions(DenyWrite = true)]
public CompilerStatus Status { get; set; }
- ///
- /// If the compiler targeted the primary directory
- ///
- [Permissions(DenyWrite = true)]
- public bool? TargetedPrimaryDirectory { get; set; }
- ///
- /// Textual output of DM
- ///
- [Permissions(DenyWrite = true)]
- public string Output { get; set; }
- ///
- /// Exit code of DM
- ///
- [Permissions(DenyWrite = true)]
- public int? ExitCode { get; set; }
- ///
- /// Git revision the compiler ran on. Not modifiable
- ///
- [Permissions(DenyWrite = true)]
- public string Revision { get; set; }
- ///
- /// Git revision of the origin branch the compiler ran on. Not modifiable
- ///
- [Permissions(DenyWrite = true)]
- public string OriginRevision { get; set; }
-
- ///
- /// How often the automatically compiles in minutes
- ///
- [Permissions(WriteRight = DreamMakerRights.SetAutoCompile)]
- public int? AutoCompileInterval { get; set; }
-
- ///
- /// The .dme file tries to compile with
- ///
- [Permissions(WriteRight = DreamMakerRights.SetDme)]
- public string TargetDme { get; set; }
}
}
diff --git a/src/Tgstation.Server.Api/Models/Instance.cs b/src/Tgstation.Server.Api/Models/Instance.cs
index 43d3eea404..bde9f13522 100644
--- a/src/Tgstation.Server.Api/Models/Instance.cs
+++ b/src/Tgstation.Server.Api/Models/Instance.cs
@@ -1,4 +1,5 @@
-using Tgstation.Server.Api.Rights;
+using System.ComponentModel.DataAnnotations;
+using Tgstation.Server.Api.Rights;
namespace Tgstation.Server.Api.Models
{
@@ -6,7 +7,7 @@ namespace Tgstation.Server.Api.Models
/// Metadata about a server instance
///
[Model(RightsType.InstanceManager, CanCrud = true, CanList = true)]
- public sealed class Instance
+ public class Instance
{
///
/// The id of the . Not modifiable
@@ -18,12 +19,14 @@ namespace Tgstation.Server.Api.Models
/// The name of the
///
[Permissions(WriteRight = InstanceManagerRights.Rename)]
+ [Required]
public string Name { get; set; }
///
- /// The path to where the is located
+ /// The path to where the is located. Changing this will temporarily offline the while it moves
///
[Permissions(WriteRight = InstanceManagerRights.Relocate)]
+ [Required]
public string Path { get; set; }
///
@@ -31,5 +34,11 @@ namespace Tgstation.Server.Api.Models
///
[Permissions(WriteRight = InstanceManagerRights.SetOnline)]
public bool Online { get; set; }
+
+ ///
+ /// If can be used on the
+ ///
+ [Permissions(WriteRight = InstanceManagerRights.SetConfiguration)]
+ public bool ConfigurationAllowed { get; set; }
}
}
diff --git a/src/Tgstation.Server.Api/Models/InstanceUser.cs b/src/Tgstation.Server.Api/Models/InstanceUser.cs
index a3e31e1dad..2173fc2681 100644
--- a/src/Tgstation.Server.Api/Models/InstanceUser.cs
+++ b/src/Tgstation.Server.Api/Models/InstanceUser.cs
@@ -6,14 +6,8 @@ namespace Tgstation.Server.Api.Models
/// Represents a s permissions in an
///
[Model(RightsType.InstanceUser, WriteRight = InstanceUserRights.WriteUsers, CanList = true, RequiresInstance = true)]
- public sealed class InstanceUser
+ public class InstanceUser
{
- ///
- /// See definition in
- ///
- [Permissions(DenyWrite = true)]
- public long Id { get; set; }
-
///
/// The of the
///
@@ -35,9 +29,9 @@ namespace Tgstation.Server.Api.Models
public RepositoryRights RepositoryRights { get; set; }
///
- /// The of the
+ /// The of the
///
- public ChatRights ChatRights { get; set; }
+ public ChatSettingsRights ChatSettingsRights { get; set; }
///
/// The of the
diff --git a/src/Tgstation.Server.Api/Models/Internal/ChatSettings.cs b/src/Tgstation.Server.Api/Models/Internal/ChatSettings.cs
new file mode 100644
index 0000000000..a672895d3c
--- /dev/null
+++ b/src/Tgstation.Server.Api/Models/Internal/ChatSettings.cs
@@ -0,0 +1,51 @@
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations.Schema;
+using Tgstation.Server.Api.Rights;
+
+namespace Tgstation.Server.Api.Models.Internal
+{
+ ///
+ /// Manage the server chat bots
+ ///
+ [Model(RightsType.ChatSettings, RequiresInstance = true)]
+ public class ChatSettings
+ {
+ ///
+ /// If the IRC client is enabled
+ ///
+ [Permissions(WriteRight = ChatSettingsRights.SetIrcEnabled)]
+ public bool IrcEnabled { get; set; }
+
+ ///
+ /// The IRC server name
+ ///
+ [Permissions(ReadRight = ChatSettingsRights.SetIrcSettings, WriteRight = ChatSettingsRights.SetIrcSettings)]
+ [Required]
+ public string IrcHost { get; set; }
+
+ ///
+ /// The IRC server port
+ ///
+ [Permissions(ReadRight = ChatSettingsRights.SetIrcSettings, WriteRight = ChatSettingsRights.SetIrcSettings)]
+ public ushort IrcPort { get; set; }
+
+ ///
+ /// The IRC server NickServ password
+ ///
+ [Permissions(ReadRight = ChatSettingsRights.SetIrcSettings, WriteRight = ChatSettingsRights.SetIrcSettings)]
+ public string IrcNickServPassword { get; set; }
+
+ ///
+ /// If the Discord bot is enabled
+ ///
+ [Permissions(WriteRight = ChatSettingsRights.SetDiscordEnabled)]
+ public bool DiscordEnabled { get; set; }
+
+ ///
+ /// The Discord bot token
+ ///
+ [Permissions(ReadRight = ChatSettingsRights.SetDiscordSettings, WriteRight = ChatSettingsRights.SetDiscordSettings)]
+ public string DiscordBotToken { get; set; }
+ }
+}
diff --git a/src/Tgstation.Server.Api/Models/Internal/CompileJob.cs b/src/Tgstation.Server.Api/Models/Internal/CompileJob.cs
new file mode 100644
index 0000000000..9fccf5e173
--- /dev/null
+++ b/src/Tgstation.Server.Api/Models/Internal/CompileJob.cs
@@ -0,0 +1,42 @@
+using System;
+using System.ComponentModel.DataAnnotations;
+
+namespace Tgstation.Server.Api.Models.Internal
+{
+ ///
+ /// Represents a run of
+ ///
+ public class CompileJob
+ {
+ ///
+ /// The ID of the job
+ ///
+ public long Id { get; set; }
+
+ ///
+ /// When the compilation started
+ ///
+ [Required]
+ public DateTimeOffset StartedAt { get; set; }
+
+ ///
+ /// When the compilation finished
+ ///
+ public DateTimeOffset FinishedAt { get; set; }
+
+ ///
+ /// If the compiler targeted the primary directory
+ ///
+ public bool? TargetedPrimaryDirectory { get; set; }
+
+ ///
+ /// Textual output of DM
+ ///
+ public string Output { get; set; }
+
+ ///
+ /// Exit code of DM. If , the job was cancelled
+ ///
+ public int? ExitCode { get; set; }
+ }
+}
diff --git a/src/Tgstation.Server.Api/Models/Internal/DreamDaemonSettings.cs b/src/Tgstation.Server.Api/Models/Internal/DreamDaemonSettings.cs
new file mode 100644
index 0000000000..6b0520c59e
--- /dev/null
+++ b/src/Tgstation.Server.Api/Models/Internal/DreamDaemonSettings.cs
@@ -0,0 +1,53 @@
+using Tgstation.Server.Api.Rights;
+
+namespace Tgstation.Server.Api.Models.Internal
+{
+ ///
+ /// Configurable settings for
+ ///
+ [Model(RightsType.DreamDaemon, CanCrud = true, RequiresInstance = true)]
+ public class DreamDaemonSettings
+ {
+ ///
+ /// If starts when it's starts
+ ///
+ [Permissions(ReadRight = DreamDaemonRights.ReadMetadata, WriteRight = DreamDaemonRights.SetAutoStart)]
+ public bool AutoStart { get; set; }
+
+ ///
+ /// If the BYOND web client can be used to connect to the game server
+ ///
+ [Permissions(ReadRight = DreamDaemonRights.ReadMetadata, WriteRight = DreamDaemonRights.SetWebClient)]
+ public bool AllowWebClient { get; set; }
+
+ ///
+ /// If the server is undergoing a soft reset. This may be automatically set by changes to other fields
+ ///
+ [Permissions(ReadRight = DreamDaemonRights.ReadMetadata, WriteRight = DreamDaemonRights.SoftRestart)]
+ public bool SoftRestart { get; set; }
+
+ ///
+ /// If the server is undergoing a soft shutdown
+ ///
+ [Permissions(ReadRight = DreamDaemonRights.ReadMetadata, WriteRight = DreamDaemonRights.SoftShutdown)]
+ public bool SoftShutdown { get; set; }
+
+ ///
+ /// The level of
+ ///
+ [Permissions(ReadRight = DreamDaemonRights.ReadMetadata, WriteRight = DreamDaemonRights.SetSecurity)]
+ public DreamDaemonSecurity SecurityLevel { get; set; }
+
+ ///
+ /// The first port uses. This should be the publically advertised port
+ ///
+ [Permissions(ReadRight = DreamDaemonRights.ReadMetadata, WriteRight = DreamDaemonRights.SetPorts)]
+ public ushort PrimaryPort { get; set; }
+
+ ///
+ /// The second port uses
+ ///
+ [Permissions(ReadRight = DreamDaemonRights.ReadMetadata, WriteRight = DreamDaemonRights.SetPorts)]
+ public ushort SecondaryPort { get; set; }
+ }
+}
diff --git a/src/Tgstation.Server.Api/Models/Internal/DreamMakerSettings.cs b/src/Tgstation.Server.Api/Models/Internal/DreamMakerSettings.cs
new file mode 100644
index 0000000000..8d31ef3ef0
--- /dev/null
+++ b/src/Tgstation.Server.Api/Models/Internal/DreamMakerSettings.cs
@@ -0,0 +1,23 @@
+using Tgstation.Server.Api.Rights;
+
+namespace Tgstation.Server.Api.Models.Internal
+{
+ ///
+ /// Configurable settings for
+ ///
+ [Model(RightsType.DreamMaker, ReadRight = DreamMakerRights.Read, CanCrud = true, RequiresInstance = true)]
+ public class DreamMakerSettings
+ {
+ ///
+ /// How often the automatically compiles in minutes
+ ///
+ [Permissions(WriteRight = DreamMakerRights.SetAutoCompile)]
+ public int? AutoCompileInterval { get; set; }
+
+ ///
+ /// The .dme file tries to compile with
+ ///
+ [Permissions(WriteRight = DreamMakerRights.SetDme)]
+ public string TargetDme { get; set; }
+ }
+}
diff --git a/src/Tgstation.Server.Api/Models/Internal/Job.cs b/src/Tgstation.Server.Api/Models/Internal/Job.cs
new file mode 100644
index 0000000000..b8ebe63b70
--- /dev/null
+++ b/src/Tgstation.Server.Api/Models/Internal/Job.cs
@@ -0,0 +1,44 @@
+using System;
+using System.ComponentModel.DataAnnotations;
+
+namespace Tgstation.Server.Api.Models.Internal
+{
+ ///
+ /// Represents a long running job
+ ///
+ [Model(RequiresInstance = true)]
+ public class Job
+ {
+ ///
+ /// The ID
+ ///
+ [Permissions(DenyWrite = true)]
+ public long Id { get; set; }
+
+ ///
+ /// English description of the
+ ///
+ [Permissions(DenyWrite = true)]
+ [Required]
+ public string Description { get; set; }
+
+ ///
+ /// When the was started
+ ///
+ [Permissions(DenyWrite = true)]
+ [Required]
+ public DateTimeOffset StartedAt { get; set; }
+
+ ///
+ /// When the stopped
+ ///
+ [Permissions(DenyWrite = true)]
+ public DateTimeOffset StoppedAt { get; set; }
+
+ ///
+ /// If the was cancelled
+ ///
+ [Permissions(DenyWrite = true)]
+ public bool Cancelled { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/src/Tgstation.Server.Api/Models/Internal/RepositorySettings.cs b/src/Tgstation.Server.Api/Models/Internal/RepositorySettings.cs
new file mode 100644
index 0000000000..11917d2f81
--- /dev/null
+++ b/src/Tgstation.Server.Api/Models/Internal/RepositorySettings.cs
@@ -0,0 +1,50 @@
+using System.ComponentModel.DataAnnotations;
+using Tgstation.Server.Api.Rights;
+
+namespace Tgstation.Server.Api.Models.Internal
+{
+ ///
+ /// Represents configurable settings for a
+ ///
+ [Model(RightsType.Repository, ReadRight = RepositoryRights.Read, RequiresInstance = true)]
+ public class RepositorySettings
+ {
+ ///
+ /// The name of the committer
+ ///
+ [Permissions(WriteRight = RepositoryRights.ChangeCommitter)]
+ [Required]
+ public string CommitterName { get; set; }
+
+ ///
+ /// The e-mail of the committer
+ ///
+ [Permissions(WriteRight = RepositoryRights.ChangeCommitter)]
+ [Required]
+ public string CommitterEmail { get; set; }
+
+ ///
+ /// The username to access the git repository with
+ ///
+ [Permissions(ReadRight = RepositoryRights.ChangeCredentials, WriteRight = RepositoryRights.ChangeCredentials)]
+ public string AccessUser { get; set; }
+
+ ///
+ /// The token/password to access the git repository with
+ ///
+ [Permissions(ReadRight = RepositoryRights.ChangeCredentials, WriteRight = RepositoryRights.ChangeCredentials)]
+ public string AccessToken { get; set; }
+
+ ///
+ /// If commits created from testmerges are pushed to the remote
+ ///
+ [Permissions(WriteRight = RepositoryRights.ChangeTestMergeCommits)]
+ public bool PushTestMergeCommits { get; set; }
+
+ ///
+ /// How often the automatically updates in minutes
+ ///
+ [Permissions(WriteRight = RepositoryRights.ChangeAutoUpdate)]
+ public int? AutoUpdateInterval { get; set; }
+ }
+}
diff --git a/src/Tgstation.Server.Api/Models/Internal/RevisionInformation.cs b/src/Tgstation.Server.Api/Models/Internal/RevisionInformation.cs
new file mode 100644
index 0000000000..44eb907a89
--- /dev/null
+++ b/src/Tgstation.Server.Api/Models/Internal/RevisionInformation.cs
@@ -0,0 +1,24 @@
+using System.ComponentModel.DataAnnotations;
+
+namespace Tgstation.Server.Api.Models.Internal
+{
+ ///
+ /// Represents information about a current git revison
+ ///
+ public class RevisionInformation
+ {
+ ///
+ /// The revision sha
+ ///
+ [Permissions(DenyWrite = true)]
+ [Required, StringLength(40)]
+ public string Revision { get; set; }
+
+ ///
+ /// The sha of the most recent remote commit
+ ///
+ [Permissions(DenyWrite = true)]
+ [Required, StringLength(40)]
+ public string OriginRevision { get; set; }
+ }
+}
diff --git a/src/Tgstation.Server.Api/Models/Internal/ServerSettings.cs b/src/Tgstation.Server.Api/Models/Internal/ServerSettings.cs
new file mode 100644
index 0000000000..f044801219
--- /dev/null
+++ b/src/Tgstation.Server.Api/Models/Internal/ServerSettings.cs
@@ -0,0 +1,29 @@
+using Tgstation.Server.Api.Rights;
+
+namespace Tgstation.Server.Api.Models.Internal
+{
+ ///
+ /// Metadata about an installation
+ ///
+ [Model(RightsType.Administration)]
+ public class ServerSettings
+ {
+ ///
+ /// Use the specified Windows/POSIX authentication group to authorize users. Changing this may enable or disable s depending on how they were configured. Setting this to changes the authentication mode to database.
+ ///
+ [Permissions(ReadRight = AdministrationRights.ChangeAuthenticationGroup, WriteRight = AdministrationRights.ChangeAuthenticationGroup)]
+ public string SystemAuthenticationGroup { get; set; }
+
+ ///
+ /// Automatically send unhandled exception data to a public collection service. This will be limited to system information, path data, and game code compilation information.
+ ///
+ [Permissions(ReadRight = AdministrationRights.ChangeTelemetry, WriteRight = AdministrationRights.ChangeTelemetry)]
+ public bool EnableTelemetry { get; set; }
+
+ ///
+ /// The git repository to recieve updates to Tgstation.Server.Host from, must include credentials if necessary. If set to upstream pulls will be disabled entirely
+ ///
+ [Permissions(ReadRight = AdministrationRights.SetUpstreamRepository, WriteRight = AdministrationRights.SetUpstreamRepository)]
+ public string UpstreamRepository { get; set; }
+ }
+}
diff --git a/src/Tgstation.Server.Api/Models/Internal/TestMerge.cs b/src/Tgstation.Server.Api/Models/Internal/TestMerge.cs
new file mode 100644
index 0000000000..9e1d04f64d
--- /dev/null
+++ b/src/Tgstation.Server.Api/Models/Internal/TestMerge.cs
@@ -0,0 +1,40 @@
+using System;
+using System.ComponentModel.DataAnnotations;
+
+namespace Tgstation.Server.Api.Models.Internal
+{
+ ///
+ /// Represents a merge of a GitHub pull request
+ ///
+ public class TestMerge : TestMergeParameters
+ {
+ ///
+ /// The ID of the
+ ///
+ public long Id { get; set; }
+
+ ///
+ /// When the was created
+ ///
+ [Required]
+ public DateTimeOffset MergedAt { get; set; }
+
+ ///
+ /// The title of the pull request
+ ///
+ [Required]
+ public string TitleAtMerge { get; set; }
+
+ ///
+ /// The body of the pull request
+ ///
+ [Required]
+ public string BodyAtMerge { get; set; }
+
+ ///
+ /// The author of the pull request
+ ///
+ [Required]
+ public string Author { get; set; }
+ }
+}
diff --git a/src/Tgstation.Server.Api/Models/Internal/User.cs b/src/Tgstation.Server.Api/Models/Internal/User.cs
new file mode 100644
index 0000000000..e8c137c04a
--- /dev/null
+++ b/src/Tgstation.Server.Api/Models/Internal/User.cs
@@ -0,0 +1,57 @@
+using System;
+using System.ComponentModel.DataAnnotations;
+using Tgstation.Server.Api.Rights;
+
+namespace Tgstation.Server.Api.Models.Internal
+{
+ ///
+ /// Represents a server
+ ///
+ [Model(RightsType.Administration, WriteRight = AdministrationRights.EditUsers, CanCrud = true)]
+ public class User
+ {
+ ///
+ /// The ID of the
+ ///
+ [Permissions(DenyWrite = true)]
+ public long Id { get; set; }
+
+ ///
+ /// If the is enabled since users cannot be deleted. System users cannot be disabled
+ ///
+ [Permissions(WriteRight = AdministrationRights.EditUsers)]
+ public bool Enabled { get; set; }
+
+ ///
+ /// When the was created
+ ///
+ [Permissions(DenyWrite = true)]
+ [Required]
+ public DateTimeOffset CreatedAt { get; set; }
+
+ ///
+ /// The SID/UID of the on Windows/POSIX respectively
+ ///
+ [Permissions(DenyWrite = true)]
+ public string SystemIdentifier { get; set; }
+
+ ///
+ /// The name of the
+ ///
+ [Permissions(WriteRight = AdministrationRights.EditUsers)]
+ [Required]
+ public string Name { get; set; }
+
+ ///
+ /// The for the
+ ///
+ [Permissions(WriteRight = AdministrationRights.EditUsers)]
+ public AdministrationRights AdministrationRights { get; set; }
+
+ ///
+ /// The for the
+ ///
+ [Permissions(WriteRight = AdministrationRights.EditUsers)]
+ public InstanceManagerRights InstanceManagerRights { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/src/Tgstation.Server.Api/Models/Job.cs b/src/Tgstation.Server.Api/Models/Job.cs
index d0231963b4..ebbf23bb21 100644
--- a/src/Tgstation.Server.Api/Models/Job.cs
+++ b/src/Tgstation.Server.Api/Models/Job.cs
@@ -1,46 +1,26 @@
-using System;
-
-namespace Tgstation.Server.Api.Models
+namespace Tgstation.Server.Api.Models
{
///
/// Represents a long running job on the server. Model is read-only, updates attempt to cancel the job
///
- [Model]
- public sealed class Job
+ public sealed class Job : Internal.Job
{
- ///
- /// The ID
- ///
- public long Id { get; set; }
-
- ///
- /// Human readable description of the
- ///
- public string Description { get; set; }
-
- ///
- /// When the was started
- ///
- public DateTimeOffset StartedAt { get; set; }
-
- ///
- /// When the stopped
- ///
- public DateTimeOffset StoppedAt { get; set; }
-
- ///
- /// If the was cancelled
- ///
- public bool Cancelled { get; set; }
-
///
/// If the has incremental progress, this will range from 1 - 100. 0 otherwise
///
+ [Permissions(DenyWrite = true)]
public int Progress { get; set; }
///
- /// If the current user has permission to cancel the job. Will be if isn't
+ /// If the current user has permission to cancel the job
///
- public bool? UserCanCancel { get; set; }
+ [Permissions(DenyWrite = true)]
+ public bool UserCanCancel { get; set; }
+
+ ///
+ /// The that started the job
+ ///
+ [Permissions(DenyWrite = true)]
+ public User StartedBy { get; set; }
}
}
diff --git a/src/Tgstation.Server.Api/Models/Repository.cs b/src/Tgstation.Server.Api/Models/Repository.cs
index 57c1a14f7d..96ca42c58c 100644
--- a/src/Tgstation.Server.Api/Models/Repository.cs
+++ b/src/Tgstation.Server.Api/Models/Repository.cs
@@ -6,8 +6,7 @@ namespace Tgstation.Server.Api.Models
///
/// Represents a git repository
///
- [Model(RightsType.Repository, ReadRight = RepositoryRights.Read, RequiresInstance = true)]
- public sealed class Repository
+ public sealed class Repository : Internal.RepositorySettings
{
///
/// The origin URL. If , the does not exist
@@ -19,7 +18,13 @@ namespace Tgstation.Server.Api.Models
/// The commit HEAD points to
///
[Permissions(WriteRight = RepositoryRights.SetSha)]
- public string Sha { get; set; }
+ public string NewRevision { get; set; }
+
+ ///
+ /// The current for the
+ ///
+ [Permissions(DenyWrite = true)]
+ public RevisionInformation RevisionInformation { get; set; }
///
/// The branch or tag HEAD points to
@@ -28,51 +33,9 @@ namespace Tgstation.Server.Api.Models
public string Reference { get; set; }
///
- /// The name of the committer
- ///
- [Permissions(WriteRight = RepositoryRights.ChangeCommitter)]
- public string CommitterName { get; set; }
-
- ///
- /// The e-mail of the committer
- ///
- [Permissions(WriteRight = RepositoryRights.ChangeCommitter)]
- public string CommitterEmail { get; set; }
-
- ///
- /// Associated list of tag name -> sha for repository compiles. Not modifiable
- ///
- [Permissions(DenyWrite = true)]
- public IReadOnlyDictionary Backups { get; set; }
-
- ///
- /// Associated list of GitHub pull request number -> sha for merged pull requests. Adding a value to this list will merge the latest commit of the pull request numbered by the key
+ /// for new s
///
[Permissions(WriteRight = RepositoryRights.MergePullRequest)]
- public Dictionary PullRequests { get; set; }
-
- ///
- /// The username to access the git repository with
- ///
- [Permissions(ReadRight = RepositoryRights.ChangeCredentials, WriteRight = RepositoryRights.ChangeCredentials)]
- public string AccessUser { get; set; }
-
- ///
- /// The token/password to access the git repository with
- ///
- [Permissions(ReadRight = RepositoryRights.ChangeCredentials, WriteRight = RepositoryRights.ChangeCredentials)]
- public string AccessToken { get; set; }
-
- ///
- /// If commits created from testmerges are pushed to the remote
- ///
- [Permissions(WriteRight = RepositoryRights.ChangeTestMergeCommits)]
- public bool PushTestMergeCommits { get; set; }
-
- ///
- /// How often the automatically updates in minutes
- ///
- [Permissions(WriteRight = RepositoryRights.ChangeAutoUpdate)]
- public int? AutoUpdateInterval { get; set; }
+ public List NewTestMerges { get; set; }
}
}
\ No newline at end of file
diff --git a/src/Tgstation.Server.Api/Models/RevisionInformation.cs b/src/Tgstation.Server.Api/Models/RevisionInformation.cs
new file mode 100644
index 0000000000..0c769f02e7
--- /dev/null
+++ b/src/Tgstation.Server.Api/Models/RevisionInformation.cs
@@ -0,0 +1,13 @@
+using System.Collections.Generic;
+
+namespace Tgstation.Server.Api.Models
+{
+ ///
+ public sealed class RevisionInformation : Internal.RevisionInformation
+ {
+ ///
+ /// The s active in the
+ ///
+ public List TestMerges { get; set; }
+ }
+}
diff --git a/src/Tgstation.Server.Api/Models/TestMerge.cs b/src/Tgstation.Server.Api/Models/TestMerge.cs
new file mode 100644
index 0000000000..d448504e2d
--- /dev/null
+++ b/src/Tgstation.Server.Api/Models/TestMerge.cs
@@ -0,0 +1,11 @@
+namespace Tgstation.Server.Api.Models
+{
+ ///
+ public sealed class TestMerge : Internal.TestMerge
+ {
+ ///
+ /// The who created the
+ ///
+ public User MergedBy { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/src/Tgstation.Server.Api/Models/TestMergeParameters.cs b/src/Tgstation.Server.Api/Models/TestMergeParameters.cs
new file mode 100644
index 0000000000..2c3ba33f02
--- /dev/null
+++ b/src/Tgstation.Server.Api/Models/TestMergeParameters.cs
@@ -0,0 +1,21 @@
+using System.ComponentModel.DataAnnotations;
+
+namespace Tgstation.Server.Api.Models
+{
+ ///
+ /// Parameters for creating a
+ ///
+ public class TestMergeParameters
+ {
+ ///
+ /// The number of the pull request
+ ///
+ public int Number { get; set; }
+
+ ///
+ /// The sha of the pull request revision to merge
+ ///
+ [Required]
+ public string PullRequestRevision { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/src/Tgstation.Server.Api/Models/Token.cs b/src/Tgstation.Server.Api/Models/Token.cs
index e970114954..9ec9a5edd5 100644
--- a/src/Tgstation.Server.Api/Models/Token.cs
+++ b/src/Tgstation.Server.Api/Models/Token.cs
@@ -1,38 +1,13 @@
-using System;
-using System.Net;
-using Tgstation.Server.Api.Rights;
-
-namespace Tgstation.Server.Api.Models
+namespace Tgstation.Server.Api.Models
{
///
- /// Represents an access token for the server. Read action generates a new one. Update action expires all for user
+ /// Represents a JWT returned by the API
///
- [Model(RightsType.Token, CanList = true)]
public sealed class Token
{
///
- /// The id of the . Not modifiable
+ /// The value of the JWT
///
- public long Id { get; }
-
- ///
- /// The user agent that created the
- ///
- public string ClientUserAgent { get; set; }
-
- ///
- /// The the was originally issued to
- ///
- public IPAddress IssuedTo { get; set; }
-
- ///
- /// When the was originally issued
- ///
- public DateTimeOffset IssuedAt { get; set; }
-
- ///
- /// The token . Not modifiable, only appears once
- ///
- public string Value { get; set; }
+ public string Bearer { get; set; }
}
-}
\ No newline at end of file
+}
diff --git a/src/Tgstation.Server.Api/Models/User.cs b/src/Tgstation.Server.Api/Models/User.cs
index fe50e0bc77..83fac4e82f 100644
--- a/src/Tgstation.Server.Api/Models/User.cs
+++ b/src/Tgstation.Server.Api/Models/User.cs
@@ -1,39 +1,12 @@
-using Tgstation.Server.Api.Rights;
-
-namespace Tgstation.Server.Api.Models
+namespace Tgstation.Server.Api.Models
{
- ///
- /// Represents a server
- ///
- [Model(RightsType.Administration, WriteRight = AdministrationRights.EditUsers)]
- public sealed class User
+ ///
+ public sealed class User : Internal.User
{
///
- /// The ID of the
+ /// The who created this
///
[Permissions(DenyWrite = true)]
- public long Id { get; set; }
-
- ///
- /// The SID/UID of the on Windows/POSIX respectively
- ///
- [Permissions(DenyWrite = true)]
- public string SystemId { get; set; }
-
- ///
- /// The name of the
- ///
- [Permissions(DenyWrite = true)]
- public string Name { get; set; }
-
- ///
- /// The for the
- ///
- public AdministrationRights AdministrationRights { get; set; }
-
- ///
- /// The for the
- ///
- public InstanceManagerRights InstanceManagerRights { get; set; }
+ public User CreatedBy { get; set; }
}
}
\ No newline at end of file
diff --git a/src/Tgstation.Server.Api/Rights/AdministrationRights.cs b/src/Tgstation.Server.Api/Rights/AdministrationRights.cs
index 5a9a37aaf8..bc7d302397 100644
--- a/src/Tgstation.Server.Api/Rights/AdministrationRights.cs
+++ b/src/Tgstation.Server.Api/Rights/AdministrationRights.cs
@@ -13,11 +13,11 @@ namespace Tgstation.Server.Api.Rights
///
None = 0,
///
- /// User can change
+ /// User can change
///
ChangeAuthenticationGroup = 1,
///
- /// User can change
+ /// User can change
///
ChangeTelemetry = 2,
///
@@ -33,9 +33,8 @@ namespace Tgstation.Server.Api.Rights
///
ChangeVersion = 16,
///
- /// User can change
+ /// User can change
///
- SetUpstreamRepository
-
+ SetUpstreamRepository = 32
}
}
diff --git a/src/Tgstation.Server.Api/Rights/ChatRights.cs b/src/Tgstation.Server.Api/Rights/ChatSettingsRights.cs
similarity index 72%
rename from src/Tgstation.Server.Api/Rights/ChatRights.cs
rename to src/Tgstation.Server.Api/Rights/ChatSettingsRights.cs
index 30fb36787a..d27723c28c 100644
--- a/src/Tgstation.Server.Api/Rights/ChatRights.cs
+++ b/src/Tgstation.Server.Api/Rights/ChatSettingsRights.cs
@@ -3,10 +3,10 @@
namespace Tgstation.Server.Api.Rights
{
///
- /// Rights for
+ /// Rights for
///
[Flags]
- public enum ChatRights
+ public enum ChatSettingsRights
{
///
/// User has no rights
@@ -21,9 +21,9 @@ namespace Tgstation.Server.Api.Rights
///
SetIrcSettings = 2,
///
- /// User can change the IRC channels
+ /// User can change the chat channels
///
- SetIrcChannels = 4,
+ SetChannels = 4,
///
/// User can enable/disable the Discord bot
///
@@ -32,9 +32,5 @@ namespace Tgstation.Server.Api.Rights
/// User can change the Discord settings
///
SetDiscordSettings = 16,
- ///
- /// User can change the Discord channels
- ///
- SetDiscordChannels = 32,
}
}
diff --git a/src/Tgstation.Server.Api/Rights/DreamDaemonRights.cs b/src/Tgstation.Server.Api/Rights/DreamDaemonRights.cs
index 51591eb334..f99f63cd58 100644
--- a/src/Tgstation.Server.Api/Rights/DreamDaemonRights.cs
+++ b/src/Tgstation.Server.Api/Rights/DreamDaemonRights.cs
@@ -13,7 +13,7 @@ namespace Tgstation.Server.Api.Rights
///
None = 0,
///
- /// User can read , , and
+ /// User can read
///
ReadRevision = 1,
///
@@ -21,27 +21,27 @@ namespace Tgstation.Server.Api.Rights
///
SetPorts = 2,
///
- /// User can change
+ /// User can change
///
SetAutoStart = 4,
///
- /// User set
+ /// User set
///
SetSecurity = 8,
///
- /// User can read all ports, , , , , and
+ /// User can read all ports, , , , , and
///
ReadMetadata = 16,
///
- /// User can change
+ /// User can change
///
SetWebClient = 32,
///
- /// User can enable
+ /// User can enable
///
SoftRestart = 64,
///
- /// User can enable
+ /// User can enable
///
SoftShutdown = 128,
///
@@ -53,7 +53,7 @@ namespace Tgstation.Server.Api.Rights
///
Shutdown = 512,
///
- /// User can start and disable and
+ /// User can start and disable and
///
Start = 1024
}
diff --git a/src/Tgstation.Server.Api/Rights/DreamMakerRights.cs b/src/Tgstation.Server.Api/Rights/DreamMakerRights.cs
index f770f8c01e..809816dbf9 100644
--- a/src/Tgstation.Server.Api/Rights/DreamMakerRights.cs
+++ b/src/Tgstation.Server.Api/Rights/DreamMakerRights.cs
@@ -25,11 +25,11 @@ namespace Tgstation.Server.Api.Rights
///
CancelCompile = 4,
///
- /// User may modify
+ /// User may modify
///
SetAutoCompile = 8,
///
- /// User may modify
+ /// User may modify
///
SetDme = 16
}
diff --git a/src/Tgstation.Server.Api/Rights/InstanceManagerRights.cs b/src/Tgstation.Server.Api/Rights/InstanceManagerRights.cs
index a00d4ffedc..106eaa1ad1 100644
--- a/src/Tgstation.Server.Api/Rights/InstanceManagerRights.cs
+++ b/src/Tgstation.Server.Api/Rights/InstanceManagerRights.cs
@@ -39,6 +39,10 @@ namespace Tgstation.Server.Api.Rights
///
/// User can view all s
///
- List = 64
+ List = 64,
+ ///
+ /// User can change
+ ///
+ SetConfiguration = 128
}
}
diff --git a/src/Tgstation.Server.Api/Rights/RepositoryRights.cs b/src/Tgstation.Server.Api/Rights/RepositoryRights.cs
index 80339d9d7f..c418507036 100644
--- a/src/Tgstation.Server.Api/Rights/RepositoryRights.cs
+++ b/src/Tgstation.Server.Api/Rights/RepositoryRights.cs
@@ -29,19 +29,19 @@ namespace Tgstation.Server.Api.Rights
///
UpdateBranch = 16,
///
- /// User may change and
+ /// User may change and
///
ChangeCommitter = 32,
///
- /// User may change
+ /// User may change
///
ChangeTestMergeCommits = 64,
///
- /// User may change
+ /// User may change
///
ChangeAutoUpdate = 128,
///
- /// User may read and change and
+ /// User may read and change and
///
ChangeCredentials = 256,
///
@@ -49,7 +49,7 @@ namespace Tgstation.Server.Api.Rights
///
SetReference = 512,
///
- /// User may read all fields in the with the exception of
+ /// User may read all fields in the with the exception of
///
Read = 1024,
}
diff --git a/src/Tgstation.Server.Api/Rights/RightsHelper.cs b/src/Tgstation.Server.Api/Rights/RightsHelper.cs
index 1dcefa440e..ab4e16de4e 100644
--- a/src/Tgstation.Server.Api/Rights/RightsHelper.cs
+++ b/src/Tgstation.Server.Api/Rights/RightsHelper.cs
@@ -15,13 +15,12 @@ namespace Tgstation.Server.Api.Rights
{
{ RightsType.Administration, typeof(AdministrationRights) },
{ RightsType.InstanceManager, typeof(InstanceManagerRights) },
- { RightsType.Token, typeof(TokenRights) },
{ RightsType.Repository, typeof(RepositoryRights) },
{ RightsType.Byond, typeof(ByondRights) },
{ RightsType.DreamMaker, typeof(DreamMakerRights) },
{ RightsType.DreamDaemon, typeof(DreamDaemonRights) },
- { RightsType.Chat, typeof(ChatRights) },
+ { RightsType.ChatSettings, typeof(ChatSettingsRights) },
{ RightsType.Configuration, typeof(ConfigurationRights) },
{ RightsType.InstanceUser, typeof(InstanceUserRights) }
};
@@ -31,6 +30,33 @@ namespace Tgstation.Server.Api.Rights
///
/// The to lookup
/// The of the given
- public static Type TypeToRight(RightsType rightsType) => typeMap[rightsType];
+ public static Type RightToType(RightsType rightsType) => typeMap[rightsType];
+
+ ///
+ /// Gets the role claim name used for a given
+ ///
+ /// The
+ /// The
+ /// A representing the claim role name
+ public static string RoleName(TRight right) => String.Concat(typeof(TRight).Name, '.', right.ToString());
+
+ ///
+ /// Gets the role claim name used for a given and
+ ///
+ /// The
+ /// The right value
+ /// A representing the claim role name
+ public static string RoleName(RightsType rightsType, Enum right)
+ {
+ var enumType = typeMap[rightsType];
+ return String.Concat(enumType.Name, '.', Enum.GetName(enumType, right));
+ }
+
+ ///
+ /// Check if a given is meant for an
+ ///
+ /// The to check
+ /// if is an instance right, otherwise
+ public static bool IsInstanceRight(RightsType rightsType) => !(rightsType == RightsType.Administration || rightsType == RightsType.InstanceManager);
}
}
diff --git a/src/Tgstation.Server.Api/Rights/RightsType.cs b/src/Tgstation.Server.Api/Rights/RightsType.cs
index b2659f8817..7ac7491661 100644
--- a/src/Tgstation.Server.Api/Rights/RightsType.cs
+++ b/src/Tgstation.Server.Api/Rights/RightsType.cs
@@ -13,11 +13,6 @@
///
///
InstanceManager,
- ///
- ///
- ///
- Token,
-
///
///
///
@@ -35,9 +30,9 @@
///
DreamDaemon,
///
- ///
+ ///
///
- Chat,
+ ChatSettings,
///
///
///
diff --git a/src/Tgstation.Server.Api/Rights/TokenRights.cs b/src/Tgstation.Server.Api/Rights/TokenRights.cs
deleted file mode 100644
index 98f5bf3f03..0000000000
--- a/src/Tgstation.Server.Api/Rights/TokenRights.cs
+++ /dev/null
@@ -1,32 +0,0 @@
-using System;
-
-namespace Tgstation.Server.Api.Rights
-{
- ///
- /// Rights for s
- ///
- [Flags]
- public enum TokenRights
- {
- ///
- /// User has no rights
- ///
- None = 0,
- ///
- /// User can create s
- ///
- Create = 1,
- ///
- /// User can delete s
- ///
- Delete = 2,
- ///
- /// User can list all their s
- ///
- List = 4,
- ///
- /// User can perform all the actions they can for themselves on behalf of other users except
- ///
- Admin = 8,
- }
-}
diff --git a/src/Tgstation.Server.Api/Tgstation.Server.Api.csproj b/src/Tgstation.Server.Api/Tgstation.Server.Api.csproj
index 942c0a5227..3da598cb1b 100644
--- a/src/Tgstation.Server.Api/Tgstation.Server.Api.csproj
+++ b/src/Tgstation.Server.Api/Tgstation.Server.Api.csproj
@@ -10,16 +10,18 @@
true
bin\Release\netstandard2.0\Tgstation.Server.Api.xml
- 1701;1702;1705;CA2227
+ 1701;1702;1705;CA2227;CA1819
latest
- 1701;1702;1705;CA2227
+ 1701;1702;1705;CA2227;CA1819
+
+
diff --git a/src/Tgstation.Server.Client/Components/IChatClient.cs b/src/Tgstation.Server.Client/Components/IChatSettingsClient.cs
similarity index 55%
rename from src/Tgstation.Server.Client/Components/IChatClient.cs
rename to src/Tgstation.Server.Client/Components/IChatSettingsClient.cs
index 4699395a6f..d8ad100b18 100644
--- a/src/Tgstation.Server.Client/Components/IChatClient.cs
+++ b/src/Tgstation.Server.Client/Components/IChatSettingsClient.cs
@@ -8,21 +8,21 @@ namespace Tgstation.Server.Client.Components
///
/// For managing the chat bots
///
- public interface IChatClient : IRightsClient
+ public interface IChatSettingsClient : IRightsClient
{
///
- /// Get the represented by the
+ /// Get the represented by the
///
/// The for the operation
- /// A resulting in the represented by the
- Task Read(CancellationToken cancellationToken);
+ /// A resulting in the represented by the
+ Task Read(CancellationToken cancellationToken);
///
- /// Updates the setttings
+ /// Updates the setttings
///
- /// The to update
+ /// The to update
/// The for the operation
/// A representing the running operation
- Task Update(Chat chat, CancellationToken cancellationToken);
+ Task Update(ChatSettings chat, CancellationToken cancellationToken);
}
}
diff --git a/src/Tgstation.Server.Client/Components/IDreamDaemonClient.cs b/src/Tgstation.Server.Client/Components/IDreamDaemonClient.cs
index 228055c9c2..1947c4170d 100644
--- a/src/Tgstation.Server.Client/Components/IDreamDaemonClient.cs
+++ b/src/Tgstation.Server.Client/Components/IDreamDaemonClient.cs
@@ -32,7 +32,7 @@ namespace Tgstation.Server.Client.Components
Task Shutdown(CancellationToken cancellationToken);
///
- /// Update . This may trigger
+ /// Update . This may trigger
///
/// The to update
/// The for the operation
diff --git a/src/Tgstation.Server.Client/Components/IDreamMakerClient.cs b/src/Tgstation.Server.Client/Components/IDreamMakerClient.cs
index 276f1d2258..ef1fb7b29b 100644
--- a/src/Tgstation.Server.Client/Components/IDreamMakerClient.cs
+++ b/src/Tgstation.Server.Client/Components/IDreamMakerClient.cs
@@ -15,7 +15,7 @@ namespace Tgstation.Server.Client.Components
///
/// The for the operation
/// A resulting in the represented by the
- Task Read(CancellationToken cancellationToken);
+ Task Read(CancellationToken cancellationToken);
///
/// Updates the setttings
diff --git a/src/Tgstation.Server.Client/Components/IInstanceClient.cs b/src/Tgstation.Server.Client/Components/IInstanceClient.cs
index baebd33d91..36b5b605f5 100644
--- a/src/Tgstation.Server.Client/Components/IInstanceClient.cs
+++ b/src/Tgstation.Server.Client/Components/IInstanceClient.cs
@@ -35,9 +35,9 @@ namespace Tgstation.Server.Client.Components
IInstanceUserClient Users { get; }
///
- /// Access the
+ /// Access the
///
- IChatClient Chat { get; }
+ IChatSettingsClient Chat { get; }
///
/// Access the
diff --git a/src/Tgstation.Server.Client/Components/ITokenClient.cs b/src/Tgstation.Server.Client/Components/ITokenClient.cs
deleted file mode 100644
index 03965132a0..0000000000
--- a/src/Tgstation.Server.Client/Components/ITokenClient.cs
+++ /dev/null
@@ -1,57 +0,0 @@
-using System.Collections.Generic;
-using System.Threading;
-using System.Threading.Tasks;
-using Tgstation.Server.Api.Models;
-using Tgstation.Server.Api.Rights;
-
-namespace Tgstation.Server.Client.Components
-{
- ///
- /// for managing s
- ///
- public interface ITokenClient: IRightsClient
- {
- ///
- /// Generate a new for the connected user/ipaddress, deleting the current one if applicable
- ///
- /// The for the operation
- /// A resulting in the new
- Task Read(CancellationToken cancellationToken);
-
- ///
- /// Gets all active s for the
- ///
- /// The for the operation
- /// A resulting a of active s for the
- Task> GetClientInfos(CancellationToken cancellationToken);
-
- ///
- /// Gets all active s for all users
- ///
- /// The for the operation
- /// A resulting a of active s for the keyed by username
- Task> GetAllInfos(CancellationToken cancellationToken);
-
- ///
- /// Invalidate the specified active
- ///
- /// The to invalidate
- /// The for the operation
- /// A representing the running operation
- Task Invalidate(Token token, CancellationToken cancellationToken);
-
- ///
- /// Invalidate all active s for a
- ///
- /// The for the operation
- /// A representing the running operation
- Task InvalidateAllClient(CancellationToken cancellationToken);
-
- ///
- /// Invalidate all active s
- ///
- /// The for the operation
- /// A representing the running operation
- Task InvalidateAll(CancellationToken cancellationToken);
- }
-}
diff --git a/src/Tgstation.Server.Client/IServerClient.cs b/src/Tgstation.Server.Client/IServerClient.cs
index aa0fb7cc39..510d609aeb 100644
--- a/src/Tgstation.Server.Client/IServerClient.cs
+++ b/src/Tgstation.Server.Client/IServerClient.cs
@@ -30,11 +30,6 @@ namespace Tgstation.Server.Client
///
int RequeryRate { get; set; }
- ///
- /// Access the
- ///
- ITokenClient Tokens { get; }
-
///
/// Access the
///
diff --git a/src/Tgstation.Server.Client/IServerClientFactory.cs b/src/Tgstation.Server.Client/IServerClientFactory.cs
index fb79446eb0..b3b3ecb506 100644
--- a/src/Tgstation.Server.Client/IServerClientFactory.cs
+++ b/src/Tgstation.Server.Client/IServerClientFactory.cs
@@ -24,7 +24,7 @@ namespace Tgstation.Server.Client
/// Create a
///
/// The URL to access tgstation-server at
- /// The to access the API with
+ /// The to access the API with
/// The initial timeout for the connection
/// The for the operation
/// A resulting in a new
diff --git a/src/Tgstation.Server.Host.Console/Properties/launchSettings.json b/src/Tgstation.Server.Host.Console/Properties/launchSettings.json
new file mode 100644
index 0000000000..5991274947
--- /dev/null
+++ b/src/Tgstation.Server.Host.Console/Properties/launchSettings.json
@@ -0,0 +1,21 @@
+{
+ "iisSettings": {
+ "windowsAuthentication": false,
+ "anonymousAuthentication": true,
+ "iisExpress": {
+ "applicationUrl": "http://localhost:51882/",
+ "sslPort": 0
+ }
+ },
+ "profiles": {
+ "Tgstation.Server.Host.Console": {
+ "commandName": "Project",
+ "workingDirectory": "bin\\Debug\\netcoreapp2.0",
+ "launchBrowser": true,
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ },
+ "applicationUrl": "http://localhost:51885/"
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Tgstation.Server.Host.Startup/IServer.cs b/src/Tgstation.Server.Host.Startup/IServer.cs
index 8dad58bdcc..299e193036 100644
--- a/src/Tgstation.Server.Host.Startup/IServer.cs
+++ b/src/Tgstation.Server.Host.Startup/IServer.cs
@@ -7,19 +7,18 @@ namespace Tgstation.Server.Host.Startup
///
/// Represents the host
///
- public interface IServer : IDisposable
+ public interface IServer
{
///
- /// The path to the updated assembly to run if any. Populated once returns
+ /// The path to the updated assembly to run if any. Populated once returns
///
string UpdatePath { get; }
///
/// Runs the
///
- /// The arguments for the
/// The for the operation
/// A representing the running operation
- Task RunAsync(string[] args, CancellationToken cancellationToken);
+ Task RunAsync(CancellationToken cancellationToken);
}
}
diff --git a/src/Tgstation.Server.Host.Startup/IServerFactory.cs b/src/Tgstation.Server.Host.Startup/IServerFactory.cs
index 43668a1049..ed92399240 100644
--- a/src/Tgstation.Server.Host.Startup/IServerFactory.cs
+++ b/src/Tgstation.Server.Host.Startup/IServerFactory.cs
@@ -5,10 +5,11 @@
///
public interface IServerFactory
{
- ///
- /// Create a
- ///
- /// A new
- IServer CreateServer();
+ ///
+ /// Create a
+ ///
+ /// The arguments for the
+ /// A new
+ IServer CreateServer(string[] args);
}
}
diff --git a/src/Tgstation.Server.Host.Watchdog/IsolatedServerFactory.cs b/src/Tgstation.Server.Host.Watchdog/IsolatedServerFactory.cs
index c7fe7f49cc..2545747721 100644
--- a/src/Tgstation.Server.Host.Watchdog/IsolatedServerFactory.cs
+++ b/src/Tgstation.Server.Host.Watchdog/IsolatedServerFactory.cs
@@ -25,8 +25,9 @@ namespace Tgstation.Server.Host.Watchdog
///
/// Loads the at and creates an from it
///
+ /// The arguments for the
/// A new
- public IServer CreateServer()
+ public IServer CreateServer(string[] args)
{
var assembly = LoadFromAssemblyPath(assemblyPath);
//find the IServerFactory implementation
@@ -35,7 +36,7 @@ namespace Tgstation.Server.Host.Watchdog
var serverFactoryImplementationType = assembly.GetTypes().Where(x => serverFactoryInterfaceType.IsAssignableFrom(x)).First();
var serverFactory = (IServerFactory)Activator.CreateInstance(serverFactoryImplementationType);
- return serverFactory.CreateServer();
+ return serverFactory.CreateServer(args);
}
//honestly have no idea what this is for, but the examples i see just return null and it seems to work just fine
diff --git a/src/Tgstation.Server.Host.Watchdog/Watchdog.cs b/src/Tgstation.Server.Host.Watchdog/Watchdog.cs
index 540955d094..ef74711534 100644
--- a/src/Tgstation.Server.Host.Watchdog/Watchdog.cs
+++ b/src/Tgstation.Server.Host.Watchdog/Watchdog.cs
@@ -43,24 +43,19 @@ namespace Tgstation.Server.Host.Watchdog
//first run the host we started with
var serverFactory = initialServerFactory;
var assemblyPath = serverFactory.GetType().Assembly.Location;
- do
- {
- string updatePath;
- using (var server = serverFactory.CreateServer())
- {
- serverFactory = null;
- await server.RunAsync(args, cancellationToken).ConfigureAwait(false);
- updatePath = server.UpdatePath;
- }
+ do
+ {
+ var server = serverFactory.CreateServer(args);
+ await server.RunAsync(cancellationToken).ConfigureAwait(false);
- if (updatePath == null)
- break;
-
- activeAssemblyDeleter.DeleteActiveAssembly(assemblyPath);
- File.Move(updatePath, assemblyPath);
- serverFactory = isolatedAssemblyLoader.CreateIsolatedServerFactory(assemblyPath);
- }
- while (!cancellationToken.IsCancellationRequested);
+ if (server.UpdatePath == null)
+ break;
+
+ activeAssemblyDeleter.DeleteActiveAssembly(assemblyPath);
+ File.Move(server.UpdatePath, assemblyPath);
+ serverFactory = isolatedAssemblyLoader.CreateIsolatedServerFactory(assemblyPath);
+ }
+ while (!cancellationToken.IsCancellationRequested);
}
}
}
diff --git a/src/Tgstation.Server.Host/Configuration/DatabaseConfiguration.cs b/src/Tgstation.Server.Host/Configuration/DatabaseConfiguration.cs
new file mode 100644
index 0000000000..4afebe14a5
--- /dev/null
+++ b/src/Tgstation.Server.Host/Configuration/DatabaseConfiguration.cs
@@ -0,0 +1,23 @@
+namespace Tgstation.Server.Host.Configuration
+{
+ ///
+ /// Configuration options for the
+ ///
+ sealed class DatabaseConfiguration
+ {
+ ///
+ /// The key for the the resides in
+ ///
+ public const string Section = "Database";
+
+ ///
+ /// The to create
+ ///
+ public DatabaseType DatabaseType { get; set; }
+
+ ///
+ /// The connection string for the database
+ ///
+ public string ConnectionString { get; set; }
+ }
+}
diff --git a/src/Tgstation.Server.Host/Configuration/DatabaseType.cs b/src/Tgstation.Server.Host/Configuration/DatabaseType.cs
new file mode 100644
index 0000000000..bbc8be7798
--- /dev/null
+++ b/src/Tgstation.Server.Host/Configuration/DatabaseType.cs
@@ -0,0 +1,21 @@
+namespace Tgstation.Server.Host.Configuration
+{
+ ///
+ /// Type of database to user
+ ///
+ enum DatabaseType
+ {
+ ///
+ /// Use Microsoft SQL Server
+ ///
+ SqlServer,
+ ///
+ /// Use MySQL/MariaDB
+ ///
+ MySql,
+ ///
+ /// Use SQLite 3
+ ///
+ Sqlite
+ }
+}
diff --git a/src/Tgstation.Server.Host/Controllers/ApiController.cs b/src/Tgstation.Server.Host/Controllers/ApiController.cs
new file mode 100644
index 0000000000..4c6bcd541f
--- /dev/null
+++ b/src/Tgstation.Server.Host/Controllers/ApiController.cs
@@ -0,0 +1,132 @@
+using Microsoft.AspNetCore.Authentication.JwtBearer;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.Mvc.Filters;
+using Microsoft.Extensions.DependencyInjection;
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.IdentityModel.Tokens.Jwt;
+using System.Security.Claims;
+using System.Threading.Tasks;
+using Tgstation.Server.Api;
+using Tgstation.Server.Api.Rights;
+using Tgstation.Server.Host.Models;
+using Tgstation.Server.Host.Security;
+
+namespace Tgstation.Server.Host.Controllers
+{
+ ///
+ /// A for API functions
+ ///
+ [Produces(ApiHeaders.ApplicationJson)]
+ [Consumes(ApiHeaders.ApplicationJson)]
+ public abstract class ApiController : Controller
+ {
+ ///
+ /// The for the operation
+ ///
+ protected ApiHeaders ApiHeaders { get; private set; }
+
+ ///
+ /// The for the operation
+ ///
+ protected IDatabaseContext DatabaseContext { get; }
+
+ ///
+ /// The for the operation
+ ///
+ protected IAuthenticationContext AuthenticationContext { get; }
+
+ ///
+ /// The for the operation
+ ///
+ protected Instance Instance { get; }
+
+ ///
+ /// Runs after a has been validated. Creates the for the
+ ///
+ /// The for the operation
+ /// A representing the running operation
+ public static async Task OnTokenValidated(TokenValidatedContext context)
+ {
+ var databaseContext = context.HttpContext.RequestServices.GetRequiredService();
+ var authenticationContextFactory = context.HttpContext.RequestServices.GetRequiredService();
+
+ var userIdClaim = context.Principal.FindFirst(JwtRegisteredClaimNames.Sub);
+
+ if (userIdClaim == default(Claim))
+ throw new InvalidOperationException("Missing required claim!");
+
+ long userId;
+ try
+ {
+ userId = Int64.Parse(userIdClaim.Value, CultureInfo.InvariantCulture);
+ }
+ catch (Exception e)
+ {
+ throw new InvalidOperationException("Failed to parse user ID!", e);
+ }
+
+ ApiHeaders apiHeaders;
+ try
+ {
+ apiHeaders = new ApiHeaders(context.HttpContext.Request.GetTypedHeaders());
+ }
+ catch
+ {
+ //let OnActionExecutionAsync handle the reponse
+ return;
+ }
+
+ await authenticationContextFactory.CreateAuthenticationContext(userId, apiHeaders.InstanceId, context.HttpContext.RequestAborted).ConfigureAwait(false);
+
+ var authenticationContext = authenticationContextFactory.CurrentAuthenticationContext;
+
+ var enumerator = Enum.GetValues(typeof(RightsType));
+ var claims = new List();
+ foreach (RightsType I in enumerator)
+ {
+ var rightInt = authenticationContext.GetRight(I);
+ var rightEnum = RightsHelper.RightToType(I);
+ var right = (Enum)Enum.ToObject(rightEnum, authenticationContext.GetRight(I));
+ foreach(Enum J in Enum.GetValues(rightEnum))
+ if(right.HasFlag(J))
+ claims.Add(new Claim(ClaimTypes.Role, RightsHelper.RoleName(I, J)));
+ }
+
+ context.Principal.AddIdentity(new ClaimsIdentity(claims));
+ }
+
+ ///
+ /// Construct an
+ ///
+ /// The value of
+ /// The for the
+ public ApiController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory)
+ {
+ DatabaseContext = databaseContext ?? throw new ArgumentNullException(nameof(databaseContext));
+ if (authenticationContextFactory == null)
+ throw new ArgumentNullException(nameof(authenticationContextFactory));
+ AuthenticationContext = authenticationContextFactory.CurrentAuthenticationContext;
+ Instance = AuthenticationContext?.InstanceUser?.Instance;
+ }
+
+ ///
+ public override async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
+ {
+ //validate the headers
+ try
+ {
+ ApiHeaders = new ApiHeaders(Request.GetTypedHeaders());
+ }
+ catch (InvalidOperationException e)
+ {
+ await BadRequest(new { message = e.Message }).ExecuteResultAsync(context).ConfigureAwait(false);
+ return;
+ }
+
+ await base.OnActionExecutionAsync(context, next).ConfigureAwait(false);
+ }
+ }
+}
diff --git a/src/Tgstation.Server.Host/Controllers/HomeController.cs b/src/Tgstation.Server.Host/Controllers/HomeController.cs
new file mode 100644
index 0000000000..d839f45017
--- /dev/null
+++ b/src/Tgstation.Server.Host/Controllers/HomeController.cs
@@ -0,0 +1,110 @@
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.EntityFrameworkCore;
+using System;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using Tgstation.Server.Api;
+using Tgstation.Server.Host.Core;
+using Tgstation.Server.Host.Models;
+using Tgstation.Server.Host.Security;
+
+namespace Tgstation.Server.Host.Controllers
+{
+ ///
+ /// Main for the
+ ///
+ [Route("/")]
+ public sealed class HomeController : ApiController
+ {
+ ///
+ /// The for the
+ ///
+ readonly ITokenFactory tokenFactory;
+ ///
+ /// The for the
+ ///
+ readonly ISystemIdentityFactory systemIdentityFactory;
+ ///
+ /// The for the
+ ///
+ readonly ICryptographySuite cryptographySuite;
+
+ ///
+ /// Construct a
+ ///
+ /// The for the
+ /// The for the
+ /// The value of
+ /// The value of
+ /// The value of
+ public HomeController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, ITokenFactory tokenFactory, ISystemIdentityFactory systemIdentityFactory, ICryptographySuite cryptographySuite) : base(databaseContext, authenticationContextFactory)
+ {
+ this.tokenFactory = tokenFactory ?? throw new ArgumentNullException(nameof(tokenFactory));
+ this.systemIdentityFactory = systemIdentityFactory ?? throw new ArgumentNullException(nameof(systemIdentityFactory));
+ this.cryptographySuite = cryptographySuite ?? throw new ArgumentNullException(nameof(cryptographySuite));
+ }
+
+ ///
+ /// Returns the version of the
+ ///
+ ///
+ [TgsAuthorize]
+ [HttpGet]
+ public JsonResult Home() => Json(Application.Version);
+
+ ///
+ /// Attempt to authenticate a using
+ ///
+ /// The for the operation
+ /// A resulting in the of the operation
+ [HttpPost]
+ public async Task CreateToken(CancellationToken cancellationToken)
+ {
+ if (ApiHeaders.IsTokenAuthentication)
+ return BadRequest(new { message = "Cannot create a token using another token!" });
+
+ var user = await DatabaseContext.Users.Where(x => x.Name == ApiHeaders.Username).Select(x => new User{
+ Id = x.Id,
+ PasswordHash = x.PasswordHash,
+ SystemIdentifier = x.SystemIdentifier,
+ Enabled = x.Enabled
+ }).FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false);
+
+ if (user == null)
+ return Unauthorized();
+
+ if (user.PasswordHash != null)
+ {
+ var originalHash = user.PasswordHash;
+ if (!cryptographySuite.CheckUserPassword(user, ApiHeaders.Password))
+ return Unauthorized();
+ if (user.PasswordHash != originalHash)
+ {
+ var updatedUser = new User
+ {
+ Id = user.Id,
+ PasswordHash = originalHash
+ };
+ DatabaseContext.Users.Attach(updatedUser);
+ updatedUser.PasswordHash = user.PasswordHash;
+ await DatabaseContext.Save(cancellationToken).ConfigureAwait(false);
+ }
+ }
+ else
+ try
+ {
+ var systemIdentity = systemIdentityFactory.CreateSystemIdentity(ApiHeaders.Username, ApiHeaders.Password);
+ }
+ catch
+ {
+ return Unauthorized();
+ }
+
+ if (!user.Enabled)
+ return Forbid();
+
+ return Json(tokenFactory.CreateToken(user));
+ }
+ }
+}
diff --git a/src/Tgstation.Server.Host/Controllers/ModelController.cs b/src/Tgstation.Server.Host/Controllers/ModelController.cs
new file mode 100644
index 0000000000..59d2b5c19a
--- /dev/null
+++ b/src/Tgstation.Server.Host/Controllers/ModelController.cs
@@ -0,0 +1,72 @@
+using Microsoft.AspNetCore.Mvc;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using Tgstation.Server.Api;
+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
+ {
+ ///
+ /// The of the
+ ///
+ protected static readonly ModelAttribute ModelAttribute = (ModelAttribute)typeof(TModel).GetCustomAttributes(typeof(ModelAttribute), true).First();
+
+ ///
+ /// Construct a
+ ///
+ /// The for the
+ /// The for the
+ public ModelController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory) : base(databaseContext, authenticationContextFactory) { }
+
+ ///
+ /// 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 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
+ ///
+ /// The being deleted
+ /// The for the operation
+ /// A resulting in the of the operation
+ [HttpDelete]
+ public virtual Task Delete([FromBody]TModel model, 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/Core/Application.cs b/src/Tgstation.Server.Host/Core/Application.cs
new file mode 100644
index 0000000000..2cedce3c5e
--- /dev/null
+++ b/src/Tgstation.Server.Host/Core/Application.cs
@@ -0,0 +1,158 @@
+using Cyberboss.AspNetCore.AsyncInitializer;
+using Microsoft.AspNetCore.Authentication.JwtBearer;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.AspNetCore.Identity;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.IdentityModel.Tokens;
+using System;
+using System.Globalization;
+using System.IdentityModel.Tokens.Jwt;
+using System.Reflection;
+using Tgstation.Server.Host.Configuration;
+using Tgstation.Server.Host.Controllers;
+using Tgstation.Server.Host.Models;
+using Tgstation.Server.Host.Security;
+
+namespace Tgstation.Server.Host.Core
+{
+ ///
+ /// Configures the ASP.NET Core web application
+ ///
+ sealed class Application
+ {
+ ///
+ /// The version of the
+ ///
+ public static readonly Version Version = Assembly.GetExecutingAssembly().GetName().Version;
+
+ ///
+ /// The for the
+ ///
+ readonly IConfiguration configuration;
+
+ ///
+ /// The for the
+ ///
+ readonly IHostingEnvironment hostingEnvironment;
+
+ ///
+ /// Construct an
+ ///
+ /// The value of
+ /// The value of
+ public Application(IConfiguration configuration, IHostingEnvironment hostingEnvironment)
+ {
+ this.configuration = configuration ?? throw new ArgumentNullException(nameof(configuration));
+ this.hostingEnvironment = hostingEnvironment ?? throw new ArgumentNullException(nameof(hostingEnvironment));
+ }
+
+ ///
+ /// Configure dependency injected services
+ ///
+ /// The to configure
+#pragma warning disable CA1822 // Mark members as static
+ public void ConfigureServices(IServiceCollection services)
+#pragma warning restore CA1822 // Mark members as static
+ {
+ if (services == null)
+ throw new ArgumentNullException(nameof(services));
+ var workingDir = Environment.CurrentDirectory;
+ var databaseConfigurationSection = configuration.GetSection(DatabaseConfiguration.Section);
+ services.Configure(databaseConfigurationSection);
+
+ services.AddOptions();
+
+ const string scheme = "JwtBearer";
+ services.AddAuthentication((options) =>
+ {
+ options.DefaultAuthenticateScheme = scheme;
+ options.DefaultChallengeScheme = scheme;
+ }).AddJwtBearer(scheme, jwtBearerOptions =>
+ {
+ jwtBearerOptions.TokenValidationParameters = new TokenValidationParameters
+ {
+ ValidateIssuerSigningKey = true,
+ IssuerSigningKey = new SymmetricSecurityKey(TokenFactory.TokenSigningKey),
+
+ ValidateIssuer = true,
+ ValidIssuer = TokenFactory.TokenIssuer,
+
+ ValidateLifetime = true,
+ ValidateAudience = true,
+ ValidAudience = TokenFactory.TokenAudience,
+
+ ClockSkew = TimeSpan.FromMinutes(5),
+
+ RequireSignedTokens = true,
+
+ RequireExpirationTime = true
+ };
+ jwtBearerOptions.Events = new JwtBearerEvents
+ {
+ OnTokenValidated = ApiController.OnTokenValidated
+ };
+ });
+ JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear(); //fucking converts 'sub' to M$ bs
+
+ services.AddMvc();
+
+ var databaseConfiguration = databaseConfigurationSection.Get();
+ void ConfigureDatabase(DbContextOptionsBuilder builder)
+ {
+ if (hostingEnvironment.IsDevelopment())
+ builder.EnableSensitiveDataLogging();
+ };
+
+ switch (databaseConfiguration.DatabaseType)
+ {
+ case DatabaseType.MySql:
+ services.AddDbContext(ConfigureDatabase);
+ services.AddScoped(x => x.GetRequiredService());
+ break;
+ case DatabaseType.Sqlite:
+ services.AddDbContext(ConfigureDatabase);
+ services.AddScoped(x => x.GetRequiredService());
+ break;
+ case DatabaseType.SqlServer:
+ services.AddDbContext(ConfigureDatabase);
+ services.AddScoped(x => x.GetRequiredService());
+ break;
+ default:
+ throw new InvalidOperationException(String.Format(CultureInfo.InvariantCulture, "Invalid {0}!", nameof(DatabaseType)));
+ }
+
+ services.AddScoped();
+
+ services.AddSingleton();
+ services.AddSingleton();
+ services.AddSingleton, PasswordHasher>();
+ services.AddSingleton();
+ services.AddSingleton();
+ }
+
+ ///
+ /// Configure the
+ ///
+ /// The to configure
+ public void Configure(IApplicationBuilder applicationBuilder)
+ {
+ if (applicationBuilder == null)
+ throw new ArgumentNullException(nameof(applicationBuilder));
+
+ if (hostingEnvironment.IsDevelopment())
+ applicationBuilder.UseDeveloperExceptionPage();
+
+ applicationBuilder.UseAsyncInitialization(async (cancellationToken) =>
+ {
+ using (var scope = applicationBuilder.ApplicationServices.CreateScope())
+ await scope.ServiceProvider.GetRequiredService().Initialize(cancellationToken).ConfigureAwait(false);
+ });
+
+ applicationBuilder.UseAuthentication();
+ applicationBuilder.UseMvc();
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Tgstation.Server.Host/Core/IServerUpdateConsumer.cs b/src/Tgstation.Server.Host/Core/IServerUpdateConsumer.cs
new file mode 100644
index 0000000000..9055b7206e
--- /dev/null
+++ b/src/Tgstation.Server.Host/Core/IServerUpdateConsumer.cs
@@ -0,0 +1,14 @@
+namespace Tgstation.Server.Host.Core
+{
+ ///
+ /// Represents a service that may take an updated assembly and run it, stopping the current assembly in the process
+ ///
+ interface IServerUpdateConsumer
+ {
+ ///
+ /// Run a new assembly and stop the current one. This will likely trigger all active s
+ ///
+ /// The path to the new assembly
+ void ApplyUpdate(string updatePath);
+ }
+}
diff --git a/src/Tgstation.Server.Host/Models/ChatChannel.cs b/src/Tgstation.Server.Host/Models/ChatChannel.cs
new file mode 100644
index 0000000000..8f57e1e885
--- /dev/null
+++ b/src/Tgstation.Server.Host/Models/ChatChannel.cs
@@ -0,0 +1,21 @@
+namespace Tgstation.Server.Host.Models
+{
+ ///
+ public sealed class ChatChannel : Api.Models.ChatChannel
+ {
+ ///
+ /// The row Id
+ ///
+ public long Id { get; set; }
+
+ ///
+ /// The
+ ///
+ public long ChatSettingsId { get; set; }
+
+ ///
+ /// The
+ ///
+ public ChatSettings ChatSettings { get; set; }
+ }
+}
diff --git a/src/Tgstation.Server.Host/Models/ChatSettings.cs b/src/Tgstation.Server.Host/Models/ChatSettings.cs
new file mode 100644
index 0000000000..5d3df56af5
--- /dev/null
+++ b/src/Tgstation.Server.Host/Models/ChatSettings.cs
@@ -0,0 +1,30 @@
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
+
+namespace Tgstation.Server.Host.Models
+{
+ ///
+ public sealed class ChatSettings : Api.Models.Internal.ChatSettings
+ {
+ ///
+ /// The row Id
+ ///
+ public long Id { get; set; }
+
+ ///
+ /// The
+ ///
+ public long InstanceId { get; set; }
+
+ ///
+ /// The parent
+ ///
+ [Required]
+ public Instance Instance { get; set; }
+
+ ///
+ /// See
+ ///
+ public List Channels { get; set; }
+ }
+}
diff --git a/src/Tgstation.Server.Host/Models/CompileJob.cs b/src/Tgstation.Server.Host/Models/CompileJob.cs
new file mode 100644
index 0000000000..29bdac135c
--- /dev/null
+++ b/src/Tgstation.Server.Host/Models/CompileJob.cs
@@ -0,0 +1,20 @@
+using System.ComponentModel.DataAnnotations;
+
+namespace Tgstation.Server.Host.Models
+{
+ ///
+ public sealed class CompileJob : Api.Models.Internal.CompileJob
+ {
+ ///
+ /// See
+ ///
+ [Required]
+ public User TriggeredBy { get; set; }
+
+ ///
+ /// See
+ ///
+ [Required]
+ public RevisionInformation RevisionInformation { get; set; }
+ }
+}
diff --git a/src/Tgstation.Server.Host/Models/DatabaseContext.cs b/src/Tgstation.Server.Host/Models/DatabaseContext.cs
new file mode 100644
index 0000000000..4dc62319ba
--- /dev/null
+++ b/src/Tgstation.Server.Host/Models/DatabaseContext.cs
@@ -0,0 +1,159 @@
+using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+using System;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using Tgstation.Server.Host.Configuration;
+using ZNetCS.AspNetCore.Logging.EntityFrameworkCore;
+
+namespace Tgstation.Server.Host.Models
+{
+ ///
+ abstract class DatabaseContext : DbContext, IDatabaseContext where TParentContext : DbContext
+ {
+ ///
+ public DbSet ServerSettings { get; set; }
+
+ ///
+ public DbSet Users { get; set; }
+
+ ///
+ public DbSet Instances { get; set; }
+
+ ///
+ /// The for s
+ ///
+ public DbSet Logs { get; set; }
+
+ ///
+ /// The s in the
+ ///
+ public DbSet InstanceUsers { get; set; }
+ ///
+ /// The s in the
+ ///
+ public DbSet ChatChannels { get; set; }
+ ///
+ /// The s in the
+ ///
+ public DbSet ChatSettings { get; set; }
+ ///
+ /// The in the
+ ///
+ public DbSet DreamDaemonSettings { get; set; }
+ ///
+ /// The in the
+ ///
+ public DbSet DreamMakerSettings { get; set; }
+ ///
+ /// The s in the
+ ///
+ public DbSet CompileJobs { get; set; }
+ ///
+ /// The s in the
+ ///
+ public DbSet Jobs { get; set; }
+ ///
+ /// The s in the
+ ///
+ public DbSet TestMerges { get; set; }
+ ///
+ /// The s in the
+ ///
+ public DbSet RevisionInformations { get; set; }
+ ///
+ /// The in the
+ ///
+ public DbSet RepositorySettings { get; set; }
+
+ ///
+ /// The connection string for the
+ ///
+ protected string ConnectionString => databaseConfiguration.ConnectionString;
+
+ ///
+ /// The for the
+ ///
+ readonly DatabaseConfiguration databaseConfiguration;
+ ///
+ /// The for the
+ ///
+ readonly ILoggerFactory loggerFactory;
+ ///
+ /// The for the
+ ///
+ readonly IDatabaseSeeder databaseSeeder;
+
+ ///
+ /// Construct a
+ ///
+ /// The for the
+ /// The containing the value of
+ /// The value of
+ /// The value of
+ public DatabaseContext(DbContextOptions dbContextOptions, IOptions databaseConfigurationOptions, ILoggerFactory loggerFactory, IDatabaseSeeder databaseSeeder) : base(dbContextOptions)
+ {
+ databaseConfiguration = databaseConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(databaseConfigurationOptions));
+ this.loggerFactory = loggerFactory ?? throw new ArgumentNullException(nameof(loggerFactory));
+ this.databaseSeeder = databaseSeeder ?? throw new ArgumentNullException(nameof(databaseSeeder));
+ }
+
+ ///
+ protected override void OnModelCreating(ModelBuilder modelBuilder)
+ {
+ base.OnModelCreating(modelBuilder);
+
+ // build default model.
+ LogModelBuilderHelper.Build(modelBuilder.Entity());
+ modelBuilder.Entity().ToTable(nameof(Logs));
+
+ modelBuilder.Entity().HasIndex(x => x.Revision).IsUnique();
+ var user = modelBuilder.Entity();
+ user.HasIndex(x => new { x.Name, x.SystemIdentifier }).IsUnique();
+ user.HasOne(x => x.CreatedBy).WithMany(x => x.CreatedUsers).OnDelete(DeleteBehavior.Restrict);
+ var chatChannel = modelBuilder.Entity();
+ chatChannel.HasIndex(x => new { x.ChatSettingsId, x.IrcChannel }).IsUnique();
+ chatChannel.HasIndex(x => new { x.ChatSettingsId, x.DiscordChannelId }).IsUnique();
+ }
+
+ ///
+ protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
+ {
+ base.OnConfiguring(optionsBuilder);
+
+ optionsBuilder.UseLoggerFactory(loggerFactory);
+ }
+
+ ///
+ public async Task GetServerSettings(CancellationToken cancellationToken)
+ {
+ var settings = await ServerSettings.FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false);
+ if (settings == default(ServerSettings))
+ {
+ settings = new ServerSettings();
+ ServerSettings.Add(settings);
+ }
+ return settings;
+ }
+
+ ///
+ public async Task Initialize(CancellationToken cancellationToken)
+ {
+#if DEBUG
+ await Database.EnsureCreatedAsync().ConfigureAwait(false);
+ var wasEmpty = (await Users.CountAsync().ConfigureAwait(false)) == 0;
+#else
+ var migrations = await Database.GetAppliedMigrationsAsync().ConfigureAwait(false);
+ var wasEmpty = !migrations.Any();
+ await Database.MigrateAsync(cancellationToken).ConfigureAwait(false);
+#endif
+ if (wasEmpty)
+ await databaseSeeder.SeedDatabase(this, cancellationToken).ConfigureAwait(false);
+ }
+
+ ///
+ public Task Save(CancellationToken cancellationToken) => SaveChangesAsync(cancellationToken);
+ }
+}
diff --git a/src/Tgstation.Server.Host/Models/DatabaseSeeder.cs b/src/Tgstation.Server.Host/Models/DatabaseSeeder.cs
new file mode 100644
index 0000000000..7ccf4f33ca
--- /dev/null
+++ b/src/Tgstation.Server.Host/Models/DatabaseSeeder.cs
@@ -0,0 +1,55 @@
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+using Tgstation.Server.Api.Rights;
+using Tgstation.Server.Host.Security;
+
+namespace Tgstation.Server.Host.Models
+{
+ ///
+ sealed class DatabaseSeeder : IDatabaseSeeder
+ {
+ ///
+ /// The default password mode admin password
+ ///
+ const string DefaultAdminPassword = "ISolemlySwearToDeleteTheDataDirectory";
+
+ ///
+ /// The default git repository to pull server updates from
+ ///
+ const string DefaultUpstreamRepository = "https://github.com/tgstation/tgstation-server";
+
+ ///
+ /// The for the
+ ///
+ readonly ICryptographySuite cryptographySuite;
+
+ ///
+ /// Construct a
+ ///
+ /// The value of
+ public DatabaseSeeder(ICryptographySuite cryptographySuite) => this.cryptographySuite = cryptographySuite ?? throw new ArgumentNullException(nameof(cryptographySuite));
+
+ ///
+ public async Task SeedDatabase(IDatabaseContext databaseContext, CancellationToken cancellationToken)
+ {
+ var admin = new User
+ {
+ AdministrationRights = (AdministrationRights)~0,
+ CreatedAt = DateTimeOffset.Now,
+ InstanceManagerRights = (InstanceManagerRights)~0,
+ Name = "Admin",
+ Enabled = true,
+ };
+ cryptographySuite.SetUserPassword(admin, DefaultAdminPassword);
+ databaseContext.Users.Add(admin);
+
+ var serverSettings = await databaseContext.GetServerSettings(cancellationToken).ConfigureAwait(false);
+
+ serverSettings.EnableTelemetry = true;
+ serverSettings.UpstreamRepository = DefaultUpstreamRepository;
+
+ await databaseContext.Save(cancellationToken).ConfigureAwait(false);
+ }
+ }
+}
diff --git a/src/Tgstation.Server.Host/Models/DreamDaemonSettings.cs b/src/Tgstation.Server.Host/Models/DreamDaemonSettings.cs
new file mode 100644
index 0000000000..6a1a550a42
--- /dev/null
+++ b/src/Tgstation.Server.Host/Models/DreamDaemonSettings.cs
@@ -0,0 +1,39 @@
+using System.ComponentModel.DataAnnotations;
+
+namespace Tgstation.Server.Host.Models
+{
+ ///
+ public sealed class DreamDaemonSettings : Api.Models.Internal.DreamDaemonSettings
+ {
+ ///
+ /// The row Id
+ ///
+ public long Id { get; set; }
+
+ ///
+ /// The PID of a currently running DD instance
+ ///
+ public int? ProcessId { get; set; }
+
+ ///
+ /// The access token used for communication with DD
+ ///
+ public string AccessToken { get; set; }
+
+ ///
+ /// The
+ ///
+ public long InstanceId { get; set; }
+
+ ///
+ /// The parent
+ ///
+ [Required]
+ public Instance Instance { get; set; }
+
+ ///
+ /// See
+ ///
+ public CompileJob CompileJob { get; set; }
+ }
+}
diff --git a/src/Tgstation.Server.Host/Models/DreamMakerSettings.cs b/src/Tgstation.Server.Host/Models/DreamMakerSettings.cs
new file mode 100644
index 0000000000..286be820a2
--- /dev/null
+++ b/src/Tgstation.Server.Host/Models/DreamMakerSettings.cs
@@ -0,0 +1,29 @@
+using System.ComponentModel.DataAnnotations;
+
+namespace Tgstation.Server.Host.Models
+{
+ ///
+ public sealed class DreamMakerSettings : Api.Models.Internal.DreamMakerSettings
+ {
+ ///
+ /// The row Id
+ ///
+ public long Id { get; set; }
+
+ ///
+ /// The
+ ///
+ public long InstanceId { get; set; }
+
+ ///
+ /// The parent
+ ///
+ [Required]
+ public Instance Instance { get; set; }
+
+ ///
+ ///
+ ///
+ public CompileJob LastJob { get; set; }
+ }
+}
diff --git a/src/Tgstation.Server.Host/Models/IDatabaseContext.cs b/src/Tgstation.Server.Host/Models/IDatabaseContext.cs
new file mode 100644
index 0000000000..9ed9acf3a4
--- /dev/null
+++ b/src/Tgstation.Server.Host/Models/IDatabaseContext.cs
@@ -0,0 +1,43 @@
+using Microsoft.EntityFrameworkCore;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Tgstation.Server.Host.Models
+{
+ ///
+ /// Represents the database
+ ///
+ public interface IDatabaseContext
+ {
+ ///
+ /// The s in the
+ ///
+ DbSet Users { get; }
+
+ ///
+ /// The s in the
+ ///
+ DbSet Instances { get; }
+
+ ///
+ /// Get the in the
+ ///
+ /// The for the operation
+ /// A resulting in the in the
+ Task GetServerSettings(CancellationToken cancellationToken);
+
+ ///
+ /// Saves changes made to the
+ ///
+ /// The for the operation
+ /// A representing the running operation
+ Task Save(CancellationToken cancellationToken);
+
+ ///
+ /// Creates and migrates the
+ ///
+ /// The for the operation
+ /// A representing the running operation
+ Task Initialize(CancellationToken cancellationToken);
+ }
+}
diff --git a/src/Tgstation.Server.Host/Models/IDatabaseSeeder.cs b/src/Tgstation.Server.Host/Models/IDatabaseSeeder.cs
new file mode 100644
index 0000000000..f57620fd0a
--- /dev/null
+++ b/src/Tgstation.Server.Host/Models/IDatabaseSeeder.cs
@@ -0,0 +1,19 @@
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Tgstation.Server.Host.Models
+{
+ ///
+ /// For initially seeding a database
+ ///
+ interface IDatabaseSeeder
+ {
+ ///
+ /// Initially seed a given
+ ///
+ /// The to seed
+ /// The for the operation
+ /// A representing the running operation
+ Task SeedDatabase(IDatabaseContext databaseContext, CancellationToken cancellationToken);
+ }
+}
diff --git a/src/Tgstation.Server.Host/Models/Instance.cs b/src/Tgstation.Server.Host/Models/Instance.cs
new file mode 100644
index 0000000000..ca3a5bfad5
--- /dev/null
+++ b/src/Tgstation.Server.Host/Models/Instance.cs
@@ -0,0 +1,50 @@
+using System.Collections.Generic;
+
+namespace Tgstation.Server.Host.Models
+{
+ ///
+ /// Represents an in the database
+ ///
+ public sealed class Instance : Api.Models.Instance
+ {
+ ///
+ /// The for the
+ ///
+ public ChatSettings ChatSettings { get; set; }
+
+ ///
+ /// The for the
+ ///
+ public DreamMakerSettings DreamMakerSettings { get; set; }
+
+ ///
+ /// The for the
+ ///
+ public DreamDaemonSettings DreamDaemonSettings { get; set; }
+
+ ///
+ /// The for the
+ ///
+ public RepositorySettings RepositorySettings { get; set; }
+
+ ///
+ /// The s in the
+ ///
+ public List InstanceUsers { get; set; }
+
+ ///
+ /// The s in the
+ ///
+ public List TestMerges { get; set; }
+
+ ///
+ /// The s in the
+ ///
+ public List CompileJobs { get; set; }
+
+ ///
+ /// The in the
+ ///
+ public List Jobs { get; set; }
+ }
+}
diff --git a/src/Tgstation.Server.Host/Models/InstanceUser.cs b/src/Tgstation.Server.Host/Models/InstanceUser.cs
new file mode 100644
index 0000000000..6566b2c891
--- /dev/null
+++ b/src/Tgstation.Server.Host/Models/InstanceUser.cs
@@ -0,0 +1,19 @@
+using System.ComponentModel.DataAnnotations;
+
+namespace Tgstation.Server.Host.Models
+{
+ ///
+ public sealed class InstanceUser : Api.Models.InstanceUser
+ {
+ ///
+ /// The row Id
+ ///
+ public long Id { get; set; }
+
+ ///
+ /// The the belongs to
+ ///
+ [Required]
+ public Instance Instance { get; set; }
+ }
+}
diff --git a/src/Tgstation.Server.Host/Models/Job.cs b/src/Tgstation.Server.Host/Models/Job.cs
new file mode 100644
index 0000000000..43d1fefba4
--- /dev/null
+++ b/src/Tgstation.Server.Host/Models/Job.cs
@@ -0,0 +1,14 @@
+using System.ComponentModel.DataAnnotations;
+
+namespace Tgstation.Server.Host.Models
+{
+ ///
+ public sealed class Job : Api.Models.Internal.Job
+ {
+ ///
+ /// See
+ ///
+ [Required]
+ public User StartedBy { get; set; }
+ }
+}
diff --git a/src/Tgstation.Server.Host/Models/Migrations/DesignTimeDbContextFactoryHelpers.cs b/src/Tgstation.Server.Host/Models/Migrations/DesignTimeDbContextFactoryHelpers.cs
new file mode 100644
index 0000000000..c633cc1ec4
--- /dev/null
+++ b/src/Tgstation.Server.Host/Models/Migrations/DesignTimeDbContextFactoryHelpers.cs
@@ -0,0 +1,29 @@
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.Options;
+using System.IO;
+using System.Reflection;
+using Tgstation.Server.Host.Configuration;
+
+namespace Tgstation.Server.Host.Models.Migrations
+{
+ ///
+ /// Contains helpers for creating design time s
+ ///
+ static class DesignTimeDbContextFactoryHelpers
+ {
+ ///
+ /// Path to the json file to use for migrations configuration
+ ///
+ const string MigrationsJson = "appsettings.Development.json";
+
+ ///
+ public static IOptions GetDbContextOptions()
+ {
+ var builder = new ConfigurationBuilder();
+ builder.SetBasePath(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location));
+ builder.AddJsonFile(MigrationsJson);
+ var configuration = builder.Build();
+ return Options.Create(configuration.GetSection(DatabaseConfiguration.Section).Get());
+ }
+ }
+}
diff --git a/src/Tgstation.Server.Host/Models/Migrations/MySqlDesignTimeDbContextFactory.cs b/src/Tgstation.Server.Host/Models/Migrations/MySqlDesignTimeDbContextFactory.cs
new file mode 100644
index 0000000000..b33b088ed2
--- /dev/null
+++ b/src/Tgstation.Server.Host/Models/Migrations/MySqlDesignTimeDbContextFactory.cs
@@ -0,0 +1,15 @@
+using Microsoft.AspNetCore.Identity;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Design;
+using Microsoft.Extensions.Logging;
+using Tgstation.Server.Host.Security;
+
+namespace Tgstation.Server.Host.Models.Migrations
+{
+ ///
+ sealed class MySqlDesignTimeDbContextFactory : IDesignTimeDbContextFactory
+ {
+ ///
+ public MySqlDatabaseContext CreateDbContext(string[] args) => new MySqlDatabaseContext(new DbContextOptions(), DesignTimeDbContextFactoryHelpers.GetDbContextOptions(), new LoggerFactory(), new DatabaseSeeder(new CryptographySuite(new PasswordHasher())));
+ }
+}
diff --git a/src/Tgstation.Server.Host/Models/Migrations/SqlServerDesignTimeDbContextFactory.cs b/src/Tgstation.Server.Host/Models/Migrations/SqlServerDesignTimeDbContextFactory.cs
new file mode 100644
index 0000000000..935768080b
--- /dev/null
+++ b/src/Tgstation.Server.Host/Models/Migrations/SqlServerDesignTimeDbContextFactory.cs
@@ -0,0 +1,15 @@
+using Microsoft.AspNetCore.Identity;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Design;
+using Microsoft.Extensions.Logging;
+using Tgstation.Server.Host.Security;
+
+namespace Tgstation.Server.Host.Models.Migrations
+{
+ ///
+ sealed class SqlServerDesignTimeDbContextFactory : IDesignTimeDbContextFactory
+ {
+ ///
+ public SqlServerDatabaseContext CreateDbContext(string[] args) => new SqlServerDatabaseContext(new DbContextOptions(), DesignTimeDbContextFactoryHelpers.GetDbContextOptions(), new LoggerFactory(), new DatabaseSeeder(new CryptographySuite(new PasswordHasher())));
+ }
+}
diff --git a/src/Tgstation.Server.Host/Models/Migrations/SqliteDesignTimeDbContextFactory.cs b/src/Tgstation.Server.Host/Models/Migrations/SqliteDesignTimeDbContextFactory.cs
new file mode 100644
index 0000000000..637bebfd0f
--- /dev/null
+++ b/src/Tgstation.Server.Host/Models/Migrations/SqliteDesignTimeDbContextFactory.cs
@@ -0,0 +1,15 @@
+using Microsoft.AspNetCore.Identity;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Design;
+using Microsoft.Extensions.Logging;
+using Tgstation.Server.Host.Security;
+
+namespace Tgstation.Server.Host.Models.Migrations
+{
+ ///
+ sealed class SqliteDesignTimeDbContextFactory : IDesignTimeDbContextFactory
+ {
+ ///
+ public SqliteDatabaseContext CreateDbContext(string[] args) => new SqliteDatabaseContext(new DbContextOptions(), DesignTimeDbContextFactoryHelpers.GetDbContextOptions(), new LoggerFactory(), new DatabaseSeeder(new CryptographySuite(new PasswordHasher())));
+ }
+}
diff --git a/src/Tgstation.Server.Host/Models/MySqlDatabaseContext.cs b/src/Tgstation.Server.Host/Models/MySqlDatabaseContext.cs
new file mode 100644
index 0000000000..e88678bf23
--- /dev/null
+++ b/src/Tgstation.Server.Host/Models/MySqlDatabaseContext.cs
@@ -0,0 +1,30 @@
+using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+using Tgstation.Server.Host.Configuration;
+
+namespace Tgstation.Server.Host.Models
+{
+ ///
+ /// for MySQL
+ ///
+ sealed class MySqlDatabaseContext : DatabaseContext
+ {
+ ///
+ /// Construct a
+ ///
+ /// The for the
+ /// The of for the
+ /// The for the
+ /// The for the
+ public MySqlDatabaseContext(DbContextOptions dbContextOptions, IOptions databaseConfiguration, ILoggerFactory loggerFactory, IDatabaseSeeder databaseSeeder) : base(dbContextOptions, databaseConfiguration, loggerFactory, databaseSeeder)
+ { }
+
+ ///
+ protected override void OnConfiguring(DbContextOptionsBuilder options)
+ {
+ base.OnConfiguring(options);
+ options.UseMySQL(ConnectionString);
+ }
+ }
+}
diff --git a/src/Tgstation.Server.Host/Models/RepositorySettings.cs b/src/Tgstation.Server.Host/Models/RepositorySettings.cs
new file mode 100644
index 0000000000..77e529f2ec
--- /dev/null
+++ b/src/Tgstation.Server.Host/Models/RepositorySettings.cs
@@ -0,0 +1,29 @@
+using System.ComponentModel.DataAnnotations;
+
+namespace Tgstation.Server.Host.Models
+{
+ ///
+ public sealed class RepositorySettings : Api.Models.Internal.RepositorySettings
+ {
+ ///
+ /// The row Id
+ ///
+ public long Id { get; set; }
+
+ ///
+ /// The
+ ///
+ public long InstanceId { get; set; }
+
+ ///
+ /// The parent
+ ///
+ [Required]
+ public Instance Instance { get; set; }
+
+ ///
+ /// See
+ ///
+ public RevisionInformation RevisionInformation { get; set; }
+ }
+}
diff --git a/src/Tgstation.Server.Host/Models/RevisionInformation.cs b/src/Tgstation.Server.Host/Models/RevisionInformation.cs
new file mode 100644
index 0000000000..057cc7e680
--- /dev/null
+++ b/src/Tgstation.Server.Host/Models/RevisionInformation.cs
@@ -0,0 +1,18 @@
+using System.Collections.Generic;
+
+namespace Tgstation.Server.Host.Models
+{
+ ///
+ public sealed class RevisionInformation : Api.Models.Internal.RevisionInformation
+ {
+ ///
+ /// The row Id
+ ///
+ public long Id { get; set; }
+
+ ///
+ /// See
+ ///
+ public List TestMerges { get; set; }
+ }
+}
diff --git a/src/Tgstation.Server.Host/Models/ServerSettings.cs b/src/Tgstation.Server.Host/Models/ServerSettings.cs
new file mode 100644
index 0000000000..4326d9f563
--- /dev/null
+++ b/src/Tgstation.Server.Host/Models/ServerSettings.cs
@@ -0,0 +1,11 @@
+namespace Tgstation.Server.Host.Models
+{
+ ///
+ public sealed class ServerSettings : Api.Models.Internal.ServerSettings
+ {
+ ///
+ /// The row Id
+ ///
+ public long Id { get; set; }
+ }
+}
diff --git a/src/Tgstation.Server.Host/Models/SqlServerDatabaseContext.cs b/src/Tgstation.Server.Host/Models/SqlServerDatabaseContext.cs
new file mode 100644
index 0000000000..240f8ba1e7
--- /dev/null
+++ b/src/Tgstation.Server.Host/Models/SqlServerDatabaseContext.cs
@@ -0,0 +1,30 @@
+using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+using Tgstation.Server.Host.Configuration;
+
+namespace Tgstation.Server.Host.Models
+{
+ ///
+ /// for Sqlserver
+ ///
+ sealed class SqlServerDatabaseContext : DatabaseContext
+ {
+ ///
+ /// Construct a
+ ///
+ /// The for the
+ /// The of for the
+ /// The for the
+ /// The for the
+ public SqlServerDatabaseContext(DbContextOptions dbContextOptions, IOptions databaseConfiguration, ILoggerFactory loggerFactory, IDatabaseSeeder databaseSeeder) : base(dbContextOptions, databaseConfiguration, loggerFactory, databaseSeeder)
+ { }
+
+ ///
+ protected override void OnConfiguring(DbContextOptionsBuilder options)
+ {
+ base.OnConfiguring(options);
+ options.UseSqlServer(ConnectionString);
+ }
+ }
+}
diff --git a/src/Tgstation.Server.Host/Models/SqliteDatabaseContext.cs b/src/Tgstation.Server.Host/Models/SqliteDatabaseContext.cs
new file mode 100644
index 0000000000..0d6c1c94e3
--- /dev/null
+++ b/src/Tgstation.Server.Host/Models/SqliteDatabaseContext.cs
@@ -0,0 +1,30 @@
+using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+using Tgstation.Server.Host.Configuration;
+
+namespace Tgstation.Server.Host.Models
+{
+ ///
+ /// for Sqlite
+ ///
+ sealed class SqliteDatabaseContext : DatabaseContext
+ {
+ ///
+ /// Construct a
+ ///
+ /// The for the
+ /// The of for the
+ /// The for the
+ /// The for the
+ public SqliteDatabaseContext(DbContextOptions dbContextOptions, IOptions databaseConfiguration, ILoggerFactory loggerFactory, IDatabaseSeeder databaseSeeder) : base(dbContextOptions, databaseConfiguration, loggerFactory, databaseSeeder)
+ { }
+
+ ///
+ protected override void OnConfiguring(DbContextOptionsBuilder options)
+ {
+ base.OnConfiguring(options);
+ options.UseSqlite(ConnectionString);
+ }
+ }
+}
diff --git a/src/Tgstation.Server.Host/Models/TestMerge.cs b/src/Tgstation.Server.Host/Models/TestMerge.cs
new file mode 100644
index 0000000000..e72fc66e3c
--- /dev/null
+++ b/src/Tgstation.Server.Host/Models/TestMerge.cs
@@ -0,0 +1,14 @@
+using System.ComponentModel.DataAnnotations;
+
+namespace Tgstation.Server.Host.Models
+{
+ ///
+ public sealed class TestMerge : Api.Models.Internal.TestMerge
+ {
+ ///
+ /// See
+ ///
+ [Required]
+ public User MergedBy { get; set; }
+ }
+}
diff --git a/src/Tgstation.Server.Host/Models/User.cs b/src/Tgstation.Server.Host/Models/User.cs
new file mode 100644
index 0000000000..dec14852f9
--- /dev/null
+++ b/src/Tgstation.Server.Host/Models/User.cs
@@ -0,0 +1,28 @@
+using System.Collections.Generic;
+
+namespace Tgstation.Server.Host.Models
+{
+ ///
+ public sealed class User : Api.Models.Internal.User
+ {
+ ///
+ /// The hash of the user's password
+ ///
+ public string PasswordHash { get; set; }
+
+ ///
+ /// See
+ ///
+ public User CreatedBy { get; set; }
+
+ ///
+ /// s created by this
+ ///
+ public List CreatedUsers { get; set; }
+
+ ///
+ /// The s for the
+ ///
+ public List InstanceUsers { get; set; }
+ }
+}
diff --git a/src/Tgstation.Server.Host/Security/AuthenticationContext.cs b/src/Tgstation.Server.Host/Security/AuthenticationContext.cs
new file mode 100644
index 0000000000..dab8b2e60a
--- /dev/null
+++ b/src/Tgstation.Server.Host/Security/AuthenticationContext.cs
@@ -0,0 +1,61 @@
+using System;
+using System.Linq;
+using Tgstation.Server.Api.Rights;
+using Tgstation.Server.Host.Models;
+
+namespace Tgstation.Server.Host.Security
+{
+ ///
+ /// Manages s for a scope
+ ///
+ sealed class AuthenticationContext : IAuthenticationContext
+ {
+ ///
+ public User User { get; }
+
+ ///
+ public InstanceUser InstanceUser { get; }
+
+ ///
+ public ISystemIdentity SystemIdentity { get; }
+
+ ///
+ /// Construct a
+ ///
+ /// The value of
+ /// The value of
+ /// The value of
+ public AuthenticationContext(ISystemIdentity systemIdentity, User user, InstanceUser instanceUser)
+ {
+ User = user ?? throw new ArgumentNullException(nameof(user));
+ if (systemIdentity == null && User.SystemIdentifier != null)
+ throw new ArgumentNullException(nameof(systemIdentity));
+ InstanceUser = instanceUser;
+ SystemIdentity = systemIdentity;
+ }
+
+ ///
+ public void Dispose() => SystemIdentity.Dispose();
+
+ ///
+ public IAuthenticationContext Clone() => new AuthenticationContext(SystemIdentity.Clone(), User, InstanceUser);
+
+ ///
+ public int GetRight(RightsType rightsType)
+ {
+ var isInstance = RightsHelper.IsInstanceRight(rightsType);
+
+ if (isInstance && InstanceUser == null)
+ return 0;
+ var rightsEnum = RightsHelper.RightToType(rightsType);
+ // use the api versions because they're the ones that contain the actual properties
+ var typeToCheck = isInstance ? typeof(InstanceUser) : typeof(User);
+
+ var prop = typeToCheck.GetProperties().Where(x => x.PropertyType == rightsEnum).First();
+
+ var right = prop.GetMethod.Invoke(isInstance ? (object)InstanceUser : User, Array.Empty