diff --git a/src/Tgstation.Server.Host/Controllers/ControlPanelController.cs b/src/Tgstation.Server.Host/Controllers/ControlPanelController.cs
index 7e450874f5..ec763dd955 100644
--- a/src/Tgstation.Server.Host/Controllers/ControlPanelController.cs
+++ b/src/Tgstation.Server.Host/Controllers/ControlPanelController.cs
@@ -30,6 +30,11 @@ namespace Tgstation.Server.Host.Controllers
///
public const string ControlPanelRoute = "/app";
+ ///
+ /// The route to the control panel channel .json.
+ ///
+ public const string ChannelJsonRoute = "channel.json";
+
///
/// Header for forcing channel.json to be fetched.
///
@@ -70,7 +75,7 @@ namespace Tgstation.Server.Host.Controllers
/// Returns the .
///
/// A with the .
- [Route("channel.json")]
+ [Route(ChannelJsonRoute)]
[HttpGet]
public IActionResult GetChannelJson()
{
diff --git a/src/Tgstation.Server.Host/Controllers/RootController.cs b/src/Tgstation.Server.Host/Controllers/RootController.cs
index 32a7987857..df363970bb 100644
--- a/src/Tgstation.Server.Host/Controllers/RootController.cs
+++ b/src/Tgstation.Server.Host/Controllers/RootController.cs
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
+using System.Linq.Expressions;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Hosting;
@@ -61,6 +62,27 @@ namespace Tgstation.Server.Host.Controllers
///
readonly ControlPanelConfiguration controlPanelConfiguration;
+ ///
+ /// Gets a giving the and action names for a given .
+ ///
+ /// An expression invoking the action on .
+ /// A containing the controller and action names.
+ static Tuple GetControlPanelActionLink(Expression> actionExpression)
+ {
+ var memberSelectorExpression = (MethodCallExpression)actionExpression.Body;
+ var method = memberSelectorExpression.Method;
+
+ var controllerName = typeof(ControlPanelController).Name;
+
+ const string ControllerSuffix = nameof(Controller);
+ if (controllerName.EndsWith(ControllerSuffix, StringComparison.Ordinal))
+ controllerName = controllerName.Substring(0, controllerName.Length - ControllerSuffix.Length);
+
+ var actionName = method.Name;
+
+ return Tuple.Create(controllerName, actionName);
+ }
+
///
/// Initializes a new instance of the class.
///
@@ -136,5 +158,19 @@ namespace Tgstation.Server.Host.Controllers
return (IActionResult?)this.TryServeFile(hostEnvironment, logger, $"{logoFileName}.svg") ?? NotFound();
}
+
+ ///
+ /// Workaround for the webpanel always expecting a trailing slash.
+ ///
+ /// A to the .
+ [HttpGet(ControlPanelController.ChannelJsonRoute)]
+ public RedirectToActionResult WebpanelChannelRedirect()
+ {
+ var actionTuple = GetControlPanelActionLink(controller => controller.GetChannelJson());
+
+ return RedirectToActionPermanent(
+ actionTuple.Item2,
+ actionTuple.Item1);
+ }
}
}
diff --git a/src/Tgstation.Server.Host/Core/Application.cs b/src/Tgstation.Server.Host/Core/Application.cs
index 702ab054da..176c946e05 100644
--- a/src/Tgstation.Server.Host/Core/Application.cs
+++ b/src/Tgstation.Server.Host/Core/Application.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Collections.Frozen;
using System.Collections.Generic;
using System.Globalization;
@@ -524,6 +524,7 @@ namespace Tgstation.Server.Host.Core
RequestPath = ControlPanelController.ControlPanelRoute,
EnableDefaultFiles = true,
EnableDirectoryBrowsing = false,
+ RedirectToAppendTrailingSlash = false,
});
}
else