diff --git a/src/Tgstation.Server.Host/Controllers/TgsOpenApiFilters.cs b/src/Tgstation.Server.Host/Controllers/TgsOpenApiFilters.cs
new file mode 100644
index 0000000000..f158398efe
--- /dev/null
+++ b/src/Tgstation.Server.Host/Controllers/TgsOpenApiFilters.cs
@@ -0,0 +1,244 @@
+using Microsoft.Net.Http.Headers;
+using Microsoft.OpenApi.Any;
+using Microsoft.OpenApi.Models;
+using Swashbuckle.AspNetCore.SwaggerGen;
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Linq;
+using System.Net;
+using Tgstation.Server.Api;
+using Tgstation.Server.Api.Models;
+using Tgstation.Server.Api.Rights;
+
+namespace Tgstation.Server.Host.Controllers
+{
+ ///
+ /// and for the server.
+ ///
+ sealed class TgsOpenApiFilters : IOperationFilter, IDocumentFilter
+ {
+ ///
+ /// The name for password authentication.
+ ///
+ public const string PasswordSecuritySchemeId = "Password_Login_Scheme";
+
+ ///
+ /// The name for token authentication.
+ ///
+ public const string TokenSecuritySchemeId = "Token_Authorization_Scheme";
+
+ const string InstanceIdParameterId = "Instance_ID_Parameter";
+ const string ApiVersionParameterId = "Api_Version_Parameter";
+ const string UserAgentParameterId = "User_Agent_Parameter";
+
+ readonly ICollection operationsToAddInstanceIdReferenceTo;
+
+ ///
+ /// Initializes a new instance of the .
+ ///
+ public TgsOpenApiFilters()
+ {
+ operationsToAddInstanceIdReferenceTo = new List();
+ }
+
+ ///
+ public void Apply(OpenApiOperation operation, OperationFilterContext context)
+ {
+ if (operation == null)
+ throw new ArgumentNullException(nameof(operation));
+ if (context == null)
+ throw new ArgumentNullException(nameof(context));
+
+ var authAttributes = context
+ .MethodInfo
+ .DeclaringType
+ .GetCustomAttributes(true)
+ .Union(
+ context
+ .MethodInfo
+ .GetCustomAttributes(true))
+ .OfType();
+
+ if (authAttributes.Any())
+ {
+ var tokenScheme = new OpenApiSecurityScheme
+ {
+ Reference = new OpenApiReference
+ {
+ Type = ReferenceType.SecurityScheme,
+ Id = TokenSecuritySchemeId
+ }
+ };
+
+ operation.Security = new List
+ {
+ new OpenApiSecurityRequirement
+ {
+ {
+ tokenScheme,
+ new List()
+ }
+ }
+ };
+
+ if (authAttributes.Any(attr => attr.RightsType.HasValue && RightsHelper.IsInstanceRight(attr.RightsType.Value)))
+ operation.Parameters.Add(new OpenApiParameter
+ {
+ Reference = new OpenApiReference
+ {
+ Type = ReferenceType.Header,
+ Id = ApiHeaders.InstanceIdHeader
+ }
+ });
+ }
+ else
+ {
+ // HomeController.CreateToken
+ var passwordScheme = new OpenApiSecurityScheme
+ {
+ Reference = new OpenApiReference
+ {
+ Type = ReferenceType.SecurityScheme,
+ Id = PasswordSecuritySchemeId
+ }
+ };
+
+ operation.Security = new List
+ {
+ new OpenApiSecurityRequirement
+ {
+ {
+ passwordScheme,
+ new List()
+ }
+ }
+ };
+ }
+ }
+
+ ///
+ public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context)
+ {
+ swaggerDoc.Components.Headers.Add(ApiHeaders.InstanceIdHeader, new OpenApiHeader
+ {
+ Description = "The instance ID being accessed",
+ Required = true,
+ Style = ParameterStyle.Simple
+ });
+
+ operationsToAddInstanceIdReferenceTo.Clear();
+
+ swaggerDoc.Components.Headers.Add(ApiHeaders.ApiVersionHeader, new OpenApiHeader
+ {
+ Description = "The API version being used in the form \"Tgstation.Server.Api/[API version]\"",
+ Required = true,
+ Style = ParameterStyle.Simple,
+ Example = new OpenApiString($"Tgstation.Server.Api/{ApiHeaders.Version}")
+ });
+
+ swaggerDoc.Components.Headers.Add(HeaderNames.UserAgent, new OpenApiHeader
+ {
+ Description = "The user agent of the calling client.",
+ Required = true,
+ Style = ParameterStyle.Simple,
+ Example = new OpenApiString("Your-user-agent/1.0.0.0")
+ });
+
+ foreach (var operation in swaggerDoc
+ .Paths
+ .SelectMany(path => path.Value.Operations)
+ .Select(kvp => kvp.Value))
+ {
+ operation.Parameters.Add(new OpenApiParameter
+ {
+ Reference = new OpenApiReference
+ {
+ Type = ReferenceType.Header,
+ Id = ApiHeaders.ApiVersionHeader
+ }
+ });
+
+ operation.Parameters.Add(new OpenApiParameter
+ {
+ Reference = new OpenApiReference
+ {
+ Type = ReferenceType.Header,
+ Id = HeaderNames.UserAgent
+ }
+ });
+ }
+
+ var errorMessageContent = new Dictionary
+ {
+ {
+ ApiHeaders.ApplicationJson,
+ new OpenApiMediaType
+ {
+ Schema = new OpenApiSchema
+ {
+ Reference = new OpenApiReference
+ {
+ Id = nameof(ErrorMessage),
+ Type = ReferenceType.Schema
+ }
+ }
+ }
+ }
+ };
+
+ void AddDefaultResponse(HttpStatusCode code, OpenApiResponse concrete)
+ {
+ string responseKey = $"{(int)code}";
+
+ swaggerDoc.Components.Responses.Add(responseKey, concrete);
+
+ var referenceResponse = new OpenApiResponse
+ {
+ Reference = new OpenApiReference
+ {
+ Type = ReferenceType.Response,
+ Id = responseKey
+ }
+ };
+
+ foreach (var path in swaggerDoc.Paths)
+ foreach (var operation in path.Value.Operations)
+ operation.Value.Responses.TryAdd(responseKey, referenceResponse);
+ }
+
+ AddDefaultResponse(HttpStatusCode.BadRequest, new OpenApiResponse
+ {
+ Description = "A badly formatted request was made. See error message for details.",
+ Content = errorMessageContent,
+ });
+
+ AddDefaultResponse(HttpStatusCode.Unauthorized, new OpenApiResponse
+ {
+ Description = "No/invalid token provided."
+ });
+
+ AddDefaultResponse(HttpStatusCode.Forbidden, new OpenApiResponse
+ {
+ Description = "User lacks sufficient permissions for the operation."
+ });
+
+ AddDefaultResponse(HttpStatusCode.Conflict, new OpenApiResponse
+ {
+ Description = "A data integrity check failed while performing the operation. See error message for details.",
+ Content = errorMessageContent
+ });
+
+ AddDefaultResponse(HttpStatusCode.InternalServerError, new OpenApiResponse
+ {
+ Description = "The server encountered an unhandled error. See error message for details.",
+ Content = errorMessageContent
+ });
+
+ AddDefaultResponse(HttpStatusCode.ServiceUnavailable, new OpenApiResponse
+ {
+ Description = "The server may be starting up or shutting down."
+ });
+ }
+ }
+}
diff --git a/src/Tgstation.Server.Host/Controllers/TgsOperationFilter.cs b/src/Tgstation.Server.Host/Controllers/TgsOperationFilter.cs
deleted file mode 100644
index 49f4fba1ce..0000000000
--- a/src/Tgstation.Server.Host/Controllers/TgsOperationFilter.cs
+++ /dev/null
@@ -1,168 +0,0 @@
-using Microsoft.OpenApi.Any;
-using Microsoft.OpenApi.Models;
-using Swashbuckle.AspNetCore.SwaggerGen;
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using Tgstation.Server.Api;
-using Tgstation.Server.Api.Models;
-using Tgstation.Server.Api.Rights;
-
-namespace Tgstation.Server.Host.Controllers
-{
- ///
- /// for the server.
- ///
- sealed class TgsOperationFilter : IOperationFilter
- {
- ///
- /// The name for password authentication.
- ///
- public const string PasswordSecuritySchemeId = "Password_Login";
-
- ///
- /// The name for token authentication.
- ///
- public const string TokenSecuritySchemeId = "Token_Authorization";
-
- ///
- public void Apply(OpenApiOperation operation, OperationFilterContext context)
- {
- if (operation == null)
- throw new ArgumentNullException(nameof(operation));
- if (context == null)
- throw new ArgumentNullException(nameof(context));
-
- var authAttributes = context
- .MethodInfo
- .DeclaringType
- .GetCustomAttributes(true)
- .Union(
- context
- .MethodInfo
- .GetCustomAttributes(true))
- .OfType();
-
- // stub var because debugger conditions are bad
- if (authAttributes.Any())
- {
- var tokenScheme = new OpenApiSecurityScheme
- {
- Reference = new OpenApiReference { Type = ReferenceType.SecurityScheme, Id = TokenSecuritySchemeId }
- };
-
- operation.Security = new List
- {
- new OpenApiSecurityRequirement
- {
- {
- tokenScheme,
- new List()
- }
- }
- };
-
- if (authAttributes.Any(attr => attr.RightsType.HasValue && RightsHelper.IsInstanceRight(attr.RightsType.Value)))
- operation.Parameters.Add(new OpenApiParameter
- {
- In = ParameterLocation.Header,
- Description = "The instance ID being accessed",
- Name = ApiHeaders.InstanceIdHeader,
- Required = true,
- Style = ParameterStyle.Simple
- });
- }
- else
- {
- // HomeController.CreateToken
- var passwordScheme = new OpenApiSecurityScheme
- {
- Reference = new OpenApiReference { Type = ReferenceType.SecurityScheme, Id = PasswordSecuritySchemeId }
- };
-
- operation.Security = new List
- {
- new OpenApiSecurityRequirement
- {
- {
- passwordScheme,
- new List()
- }
- }
- };
- }
-
- operation.Parameters.Add(new OpenApiParameter
- {
- In = ParameterLocation.Header,
- Description = "The API version being used in the form \"Tgstation.Server.Api/[API version]\"",
- Name = ApiHeaders.ApiVersionHeader,
- Required = true,
- Style = ParameterStyle.Simple,
- Example = new OpenApiString($"Tgstation.Server.Api/{ApiHeaders.Version}")
- });
-
- operation.Parameters.Add(new OpenApiParameter
- {
- In = ParameterLocation.Header,
- Description = "The user agent of the calling client.",
- Name = "User-Agent",
- Required = true,
- Style = ParameterStyle.Simple,
- Example = new OpenApiString("Your-user-agent/1.0.0.0")
- });
-
- var errorMessageContent = new Dictionary
- {
- {
- ApiHeaders.ApplicationJson,
- new OpenApiMediaType
- {
- Schema = new OpenApiSchema
- {
- Reference = new OpenApiReference
- {
- Id = nameof(ErrorMessage),
- Type = ReferenceType.Schema
- }
- }
- }
- }
- };
-
- // Add default common status codes
- operation.Responses.TryAdd("400", new OpenApiResponse
- {
- Description = "A badly formatted request was made. See error message for details.",
- Content = errorMessageContent
- });
-
- operation.Responses.TryAdd("401", new OpenApiResponse
- {
- Description = "No/invalid token provided."
- });
-
- operation.Responses.TryAdd("403", new OpenApiResponse
- {
- Description = "User lacks sufficient permissions for the operation."
- });
-
- operation.Responses.TryAdd("409", new OpenApiResponse
- {
- Description = "A data integrity check failed while performing the operation. See error message for details.",
- Content = errorMessageContent
- });
-
- operation.Responses.TryAdd("500", new OpenApiResponse
- {
- Description = "The server encountered an unhandled error. See error message for details.",
- Content = errorMessageContent
- });
-
- operation.Responses.TryAdd("503", new OpenApiResponse
- {
- Description = "The server may be starting up or shutting down."
- });
- }
- }
-}
diff --git a/src/Tgstation.Server.Host/Core/Application.cs b/src/Tgstation.Server.Host/Core/Application.cs
index 63ef876d7c..9939ea6778 100644
--- a/src/Tgstation.Server.Host/Core/Application.cs
+++ b/src/Tgstation.Server.Host/Core/Application.cs
@@ -256,9 +256,16 @@ namespace Tgstation.Server.Host.Core
Version = "v4"
});
- c.OperationFilter();
+ // Important to do this before applying our own filters
+ // Otherwise we'll get NullReferenceExceptions on parameters to be setup in our document filter
+ var assemblyLocation = Assembly.GetExecutingAssembly().Location;
+ var filePath = ioManager.ConcatPath(ioManager.GetDirectoryName(assemblyLocation), String.Concat(ioManager.GetFileNameWithoutExtension(assemblyLocation), ".xml"));
+ c.IncludeXmlComments(filePath);
- c.AddSecurityDefinition(TgsOperationFilter.PasswordSecuritySchemeId, new OpenApiSecurityScheme
+ c.OperationFilter();
+ c.DocumentFilter();
+
+ c.AddSecurityDefinition(TgsOpenApiFilters.PasswordSecuritySchemeId, new OpenApiSecurityScheme
{
In = ParameterLocation.Header,
Type = SecuritySchemeType.Http,
@@ -266,7 +273,7 @@ namespace Tgstation.Server.Host.Core
Scheme = ApiHeaders.BasicAuthenticationScheme
});
- c.AddSecurityDefinition(TgsOperationFilter.TokenSecuritySchemeId, new OpenApiSecurityScheme
+ c.AddSecurityDefinition(TgsOpenApiFilters.TokenSecuritySchemeId, new OpenApiSecurityScheme
{
BearerFormat = "JWT",
In = ParameterLocation.Header,
@@ -274,9 +281,6 @@ namespace Tgstation.Server.Host.Core
Name = HeaderNames.Authorization,
Scheme = ApiHeaders.JwtAuthenticationScheme
});
-
- var filePath = ioManager.ConcatPath(ioManager.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "Tgstation.Server.Host.xml");
- c.IncludeXmlComments(filePath);
});
// enable browser detection