diff --git a/src/Tgstation.Server.Host/Authority/IUserAuthority.cs b/src/Tgstation.Server.Host/Authority/IUserAuthority.cs
index 0279483f5c..71f807371a 100644
--- a/src/Tgstation.Server.Host/Authority/IUserAuthority.cs
+++ b/src/Tgstation.Server.Host/Authority/IUserAuthority.cs
@@ -28,10 +28,11 @@ namespace Tgstation.Server.Host.Authority
///
/// The of the .
/// If related entities should be loaded.
+ /// If the may be returned.
/// The for the operation.
/// A resulting in a .
[TgsAuthorize(AdministrationRights.ReadUsers)]
- public ValueTask> GetId(long id, bool includeJoins, CancellationToken cancellationToken);
+ public ValueTask> GetId(long id, bool includeJoins, bool allowSystemUser, CancellationToken cancellationToken);
///
/// Gets all registered s.
diff --git a/src/Tgstation.Server.Host/Authority/UserAuthority.cs b/src/Tgstation.Server.Host/Authority/UserAuthority.cs
index 2d113a7abb..fe15593a1b 100644
--- a/src/Tgstation.Server.Host/Authority/UserAuthority.cs
+++ b/src/Tgstation.Server.Host/Authority/UserAuthority.cs
@@ -47,7 +47,7 @@ namespace Tgstation.Server.Host.Authority
=> ValueTask.FromResult(new AuthorityResponse(authenticationContext.User));
///
- public async ValueTask> GetId(long id, bool includeJoins, CancellationToken cancellationToken)
+ public async ValueTask> GetId(long id, bool includeJoins, bool allowSystemUser, CancellationToken cancellationToken)
{
var queryable = ListCore(includeJoins);
@@ -57,7 +57,7 @@ namespace Tgstation.Server.Host.Authority
if (user == default)
return NotFound();
- if (user.CanonicalName == User.CanonicalizeName(User.TgsSystemUserName))
+ if (!allowSystemUser && user.CanonicalName == User.CanonicalizeName(User.TgsSystemUserName))
return Forbid();
return new AuthorityResponse(user);
diff --git a/src/Tgstation.Server.Host/Controllers/UserController.cs b/src/Tgstation.Server.Host/Controllers/UserController.cs
index 507eb1978c..e59df1f889 100644
--- a/src/Tgstation.Server.Host/Controllers/UserController.cs
+++ b/src/Tgstation.Server.Host/Controllers/UserController.cs
@@ -414,7 +414,7 @@ namespace Tgstation.Server.Host.Controllers
return await userAuthority.InvokeTransformable(
this,
- authority => authority.GetId(id, true, cancellationToken));
+ authority => authority.GetId(id, true, false, cancellationToken));
}
///
diff --git a/src/Tgstation.Server.Host/Core/Application.cs b/src/Tgstation.Server.Host/Core/Application.cs
index 2ef310428e..5da177e23e 100644
--- a/src/Tgstation.Server.Host/Core/Application.cs
+++ b/src/Tgstation.Server.Host/Core/Application.cs
@@ -300,6 +300,7 @@ namespace Tgstation.Server.Host.Core
.AddErrorFilter()
.AddType()
.AddType()
+ .AddType()
.AddType()
.BindRuntimeType()
.AddQueryType()
diff --git a/src/Tgstation.Server.Host/GraphQL/Types/IGateway.cs b/src/Tgstation.Server.Host/GraphQL/Interfaces/IGateway.cs
similarity index 92%
rename from src/Tgstation.Server.Host/GraphQL/Types/IGateway.cs
rename to src/Tgstation.Server.Host/GraphQL/Interfaces/IGateway.cs
index 45b83f87c7..89acbc10cc 100644
--- a/src/Tgstation.Server.Host/GraphQL/Types/IGateway.cs
+++ b/src/Tgstation.Server.Host/GraphQL/Interfaces/IGateway.cs
@@ -2,13 +2,13 @@
using HotChocolate.Authorization;
using Microsoft.Extensions.Options;
-
using Tgstation.Server.Api.Models.Internal;
using Tgstation.Server.Host.Configuration;
+using Tgstation.Server.Host.GraphQL.Types;
using Tgstation.Server.Host.Security.OAuth;
using Tgstation.Server.Host.System;
-namespace Tgstation.Server.Host.GraphQL.Types
+namespace Tgstation.Server.Host.GraphQL.Interfaces
{
///
/// Management interface for the parent .
diff --git a/src/Tgstation.Server.Host/GraphQL/Interfaces/IUserName.cs b/src/Tgstation.Server.Host/GraphQL/Interfaces/IUserName.cs
new file mode 100644
index 0000000000..f2a57702cc
--- /dev/null
+++ b/src/Tgstation.Server.Host/GraphQL/Interfaces/IUserName.cs
@@ -0,0 +1,21 @@
+using HotChocolate.Types.Relay;
+
+namespace Tgstation.Server.Host.GraphQL.Interfaces
+{
+ ///
+ /// A lightly scoped .
+ ///
+ public interface IUserName
+ {
+ ///
+ /// The ID of the user.
+ ///
+ [ID]
+ public long Id { get; }
+
+ ///
+ /// The name of the user.
+ ///
+ public string Name { get; }
+ }
+}
diff --git a/src/Tgstation.Server.Host/GraphQL/Types/LocalGateway.cs b/src/Tgstation.Server.Host/GraphQL/Types/LocalGateway.cs
index 5892a49f01..1774bc6596 100644
--- a/src/Tgstation.Server.Host/GraphQL/Types/LocalGateway.cs
+++ b/src/Tgstation.Server.Host/GraphQL/Types/LocalGateway.cs
@@ -6,6 +6,7 @@ using Microsoft.Extensions.Options;
using Tgstation.Server.Api.Models.Internal;
using Tgstation.Server.Host.Configuration;
+using Tgstation.Server.Host.GraphQL.Interfaces;
using Tgstation.Server.Host.Security.OAuth;
using Tgstation.Server.Host.System;
diff --git a/src/Tgstation.Server.Host/GraphQL/Types/NamedEntity.cs b/src/Tgstation.Server.Host/GraphQL/Types/NamedEntity.cs
index e48c18ce14..a52677b834 100644
--- a/src/Tgstation.Server.Host/GraphQL/Types/NamedEntity.cs
+++ b/src/Tgstation.Server.Host/GraphQL/Types/NamedEntity.cs
@@ -1,5 +1,7 @@
using System;
+using Tgstation.Server.Host.GraphQL.Interfaces;
+
namespace Tgstation.Server.Host.GraphQL.Types
{
///
@@ -12,6 +14,16 @@ namespace Tgstation.Server.Host.GraphQL.Types
///
public string Name { get; }
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The to copy.
+ protected NamedEntity(NamedEntity copy)
+ : base(copy?.Id ?? throw new ArgumentNullException(nameof(copy)))
+ {
+ Name = copy.Name;
+ }
+
///
/// Initializes a new instance of the class.
///
diff --git a/src/Tgstation.Server.Host/GraphQL/Types/Node.cs b/src/Tgstation.Server.Host/GraphQL/Types/Node.cs
index dac5cd5a84..f42183950d 100644
--- a/src/Tgstation.Server.Host/GraphQL/Types/Node.cs
+++ b/src/Tgstation.Server.Host/GraphQL/Types/Node.cs
@@ -5,6 +5,7 @@ using HotChocolate;
using Microsoft.Extensions.Options;
using Tgstation.Server.Host.Configuration;
+using Tgstation.Server.Host.GraphQL.Interfaces;
namespace Tgstation.Server.Host.GraphQL.Types
{
diff --git a/src/Tgstation.Server.Host/GraphQL/Types/RemoteGateway.cs b/src/Tgstation.Server.Host/GraphQL/Types/RemoteGateway.cs
index ffe7731895..0a00849057 100644
--- a/src/Tgstation.Server.Host/GraphQL/Types/RemoteGateway.cs
+++ b/src/Tgstation.Server.Host/GraphQL/Types/RemoteGateway.cs
@@ -7,6 +7,7 @@ using Microsoft.Extensions.Options;
using Tgstation.Server.Api.Models;
using Tgstation.Server.Api.Models.Internal;
using Tgstation.Server.Host.Configuration;
+using Tgstation.Server.Host.GraphQL.Interfaces;
using Tgstation.Server.Host.Security.OAuth;
using Tgstation.Server.Host.System;
diff --git a/src/Tgstation.Server.Host/GraphQL/Types/User.cs b/src/Tgstation.Server.Host/GraphQL/Types/User.cs
index f32c6ff2cd..4ab07675df 100644
--- a/src/Tgstation.Server.Host/GraphQL/Types/User.cs
+++ b/src/Tgstation.Server.Host/GraphQL/Types/User.cs
@@ -1,13 +1,20 @@
using System;
using System.Collections.Generic;
+using System.Threading;
using System.Threading.Tasks;
+using HotChocolate;
+
+using Tgstation.Server.Host.Authority;
+using Tgstation.Server.Host.Authority.Core;
+using Tgstation.Server.Host.GraphQL.Interfaces;
+
namespace Tgstation.Server.Host.GraphQL.Types
{
///
/// A user registered in the server.
///
- public sealed class User : NamedEntity
+ public sealed class User : NamedEntity, IUserName
{
///
/// If the is enabled since users cannot be deleted. System users cannot be disabled.
@@ -72,9 +79,23 @@ namespace Tgstation.Server.Host.GraphQL.Types
///
/// The who created this .
///
- /// A resulting in the who created this , if any.
- public ValueTask CreatedBy()
- => throw new NotImplementedException();
+ /// The .
+ /// The for the operation.
+ /// The that created this , if any.
+ public async ValueTask CreatedBy(
+ [Service] IGraphQLAuthorityInvoker userAuthority,
+ CancellationToken cancellationToken)
+ {
+ ArgumentNullException.ThrowIfNull(userAuthority);
+ if (!createdById.HasValue)
+ return null;
+
+ var user = await userAuthority.InvokeTransformable(authority => authority.GetId(createdById.Value, false, true, cancellationToken));
+ if (user.CanonicalName == Models.User.CanonicalizeName(Models.User.TgsSystemUserName))
+ return new UserName(user);
+
+ return user;
+ }
///
/// List of s associated with the user if OAuth is configured.
diff --git a/src/Tgstation.Server.Host/GraphQL/Types/UserName.cs b/src/Tgstation.Server.Host/GraphQL/Types/UserName.cs
new file mode 100644
index 0000000000..f16379debc
--- /dev/null
+++ b/src/Tgstation.Server.Host/GraphQL/Types/UserName.cs
@@ -0,0 +1,19 @@
+using Tgstation.Server.Host.GraphQL.Interfaces;
+
+namespace Tgstation.Server.Host.GraphQL.Types
+{
+ ///
+ /// A with limited fields.
+ ///
+ public sealed class UserName : NamedEntity, IUserName
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The to copy.
+ public UserName(NamedEntity copy)
+ : base(copy)
+ {
+ }
+ }
+}
diff --git a/src/Tgstation.Server.Host/GraphQL/Types/Users.cs b/src/Tgstation.Server.Host/GraphQL/Types/Users.cs
index 9ff2e2b256..754771e917 100644
--- a/src/Tgstation.Server.Host/GraphQL/Types/Users.cs
+++ b/src/Tgstation.Server.Host/GraphQL/Types/Users.cs
@@ -51,7 +51,7 @@ namespace Tgstation.Server.Host.GraphQL.Types
CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(userAuthority);
- return await userAuthority.InvokeTransformable(authority => authority.GetId(id, false, cancellationToken));
+ return await userAuthority.InvokeTransformable(authority => authority.GetId(id, false, false, cancellationToken));
}
///