From dc656bddf120368fe9d937805c452551dc8c286e Mon Sep 17 00:00:00 2001 From: Neil Dorin Date: Thu, 16 Apr 2026 21:31:42 -0600 Subject: [PATCH] feat: Implement login request handler with token generation and error handling --- src/PepperDash.Core/Web/WebApiServer.cs | 2 + .../Web/EssentialsWebApi.cs | 6 + .../Web/EssentialsWebApiHelpers.cs | 8 +- .../GetRoutingDevicesAndTieLinesHandler.cs | 110 +++++++++++++++- .../RequestHandlers/LoginRequestHandler.cs | 121 ++++++++++++++++++ 5 files changed, 241 insertions(+), 6 deletions(-) create mode 100644 src/PepperDash.Essentials.Core/Web/RequestHandlers/LoginRequestHandler.cs diff --git a/src/PepperDash.Core/Web/WebApiServer.cs b/src/PepperDash.Core/Web/WebApiServer.cs index bccd52aa..814a6f1d 100644 --- a/src/PepperDash.Core/Web/WebApiServer.cs +++ b/src/PepperDash.Core/Web/WebApiServer.cs @@ -75,6 +75,8 @@ public class WebApiServer : IKeyName if (_server == null) _server = new HttpCwsServer(BasePath); + _server.AuthenticateAllRoutes = false; + _server.setProcessName(Key); _server.HttpRequestHandler = new DefaultRequestHandler(); _server.ReceivedRequestEvent += ReceivedRequestEventHandler; diff --git a/src/PepperDash.Essentials.Core/Web/EssentialsWebApi.cs b/src/PepperDash.Essentials.Core/Web/EssentialsWebApi.cs index a1b81630..eaf3bab8 100644 --- a/src/PepperDash.Essentials.Core/Web/EssentialsWebApi.cs +++ b/src/PepperDash.Essentials.Core/Web/EssentialsWebApi.cs @@ -69,6 +69,7 @@ public class EssentialsWebApi : EssentialsDevice _server = new WebApiServer(Key, Name, BasePath); + _debugServer = new WebApiServer($"{key}-debug-app", $"{name} Debug App", "/debug"); _debugServer.SetFallbackHandler(new ServeDebugAppRequestHandler()); @@ -79,6 +80,11 @@ public class EssentialsWebApi : EssentialsDevice { var routes = new List { + new HttpCwsRoute("login") + { + Name = "Root", + RouteHandler = new LoginRequestHandler() + }, new HttpCwsRoute("versions") { Name = "ReportVersions", diff --git a/src/PepperDash.Essentials.Core/Web/EssentialsWebApiHelpers.cs b/src/PepperDash.Essentials.Core/Web/EssentialsWebApiHelpers.cs index 243667c0..bb7e8502 100644 --- a/src/PepperDash.Essentials.Core/Web/EssentialsWebApiHelpers.cs +++ b/src/PepperDash.Essentials.Core/Web/EssentialsWebApiHelpers.cs @@ -40,12 +40,18 @@ public static class EssentialsWebApiHelpers /// public static object MapToDeviceListObject(IKeyed device) { + var interfaces = device.GetType() + .GetInterfaces() + .Select(i => i.Name) + .ToList(); + return new { device.Key, Name = (device is IKeyName) ? (device as IKeyName).Name - : "---" + : "---", + Interfaces = interfaces }; } diff --git a/src/PepperDash.Essentials.Core/Web/RequestHandlers/GetRoutingDevicesAndTieLinesHandler.cs b/src/PepperDash.Essentials.Core/Web/RequestHandlers/GetRoutingDevicesAndTieLinesHandler.cs index 72a6757f..f924c951 100644 --- a/src/PepperDash.Essentials.Core/Web/RequestHandlers/GetRoutingDevicesAndTieLinesHandler.cs +++ b/src/PepperDash.Essentials.Core/Web/RequestHandlers/GetRoutingDevicesAndTieLinesHandler.cs @@ -40,26 +40,26 @@ namespace PepperDash.Essentials.Core.Web.RequestHandlers if (device is IRoutingInputs inputDevice) { deviceInfo.HasInputs = true; - deviceInfo.InputPorts = inputDevice.InputPorts.Select(p => new PortInfo + deviceInfo.InputPorts = [.. inputDevice.InputPorts.Select(p => new PortInfo { Key = p.Key, SignalType = p.Type.ToString(), ConnectionType = p.ConnectionType.ToString(), IsInternal = p.IsInternal - }).ToList(); + })]; } // Check if device implements IRoutingOutputs if (device is IRoutingOutputs outputDevice) { deviceInfo.HasOutputs = true; - deviceInfo.OutputPorts = outputDevice.OutputPorts.Select(p => new PortInfo + deviceInfo.OutputPorts = [.. outputDevice.OutputPorts.Select(p => new PortInfo { Key = p.Key, SignalType = p.Type.ToString(), ConnectionType = p.ConnectionType.ToString(), IsInternal = p.IsInternal - }).ToList(); + })]; } // Check if device implements IRoutingInputsOutputs @@ -86,10 +86,31 @@ namespace PepperDash.Essentials.Core.Web.RequestHandlers IsInternal = tl.IsInternal }).ToList(); + // Get current active routes from DefaultCollection, grouped by signal type + var currentRoutes = RouteDescriptorCollection.DefaultCollection.Descriptors + .GroupBy(d => d.SignalType.ToString()) + .Select(g => new CurrentRouteGroupInfo + { + SignalType = g.Key, + Routes = [.. g.Select(d => new ActiveRouteInfo + { + SourceDeviceKey = d.Source.Key, + DestinationDeviceKey = d.Destination.Key, + DestinationInputPortKey = d.InputPort?.Key, + Steps = [.. d.Routes.Select(r => new RouteSwitchStepInfo + { + SwitchingDeviceKey = r.SwitchingDevice?.Key, + InputPortKey = r.InputPort?.Key, + OutputPortKey = r.OutputPort?.Key + })] + })] + }).ToList(); + var response = new RoutingSystemInfo { Devices = devices, - TieLines = tielines + TieLines = tielines, + CurrentRoutes = currentRoutes }; var jsonResponse = JsonConvert.SerializeObject(response, Formatting.Indented); @@ -121,6 +142,12 @@ namespace PepperDash.Essentials.Core.Web.RequestHandlers /// [JsonProperty("tieLines")] public List TieLines { get; set; } + + /// + /// Gets or sets the current active routes in the system, grouped by signal type + /// + [JsonProperty("currentRoutes")] + public List CurrentRoutes { get; set; } } /// @@ -238,4 +265,77 @@ namespace PepperDash.Essentials.Core.Web.RequestHandlers [JsonProperty("isInternal")] public bool IsInternal { get; set; } } + + /// + /// Represents a group of active routes for a given signal type + /// + public class CurrentRouteGroupInfo + { + /// + /// Gets or sets the signal type for the group of active routes (e.g., AudioVideo, Audio, Video, etc.) + /// + [JsonProperty("signalType")] + public string SignalType { get; set; } + + /// + /// Gets or sets the list of active routes for the given signal type + /// + [JsonProperty("routes")] + public List Routes { get; set; } + } + + /// + /// Represents a single active route from a source to a destination + /// + public class ActiveRouteInfo + { + /// + /// Gets or sets the key of the source device for the active route + /// + [JsonProperty("sourceDeviceKey")] + public string SourceDeviceKey { get; set; } + + /// + /// Gets or sets the key of the destination device for the active route + /// + [JsonProperty("destinationDeviceKey")] + public string DestinationDeviceKey { get; set; } + + /// + /// Gets or sets the key of the destination input port for the active route, if applicable + /// + [JsonProperty("destinationInputPortKey", NullValueHandling = NullValueHandling.Ignore)] + public string DestinationInputPortKey { get; set; } + + /// + /// Gets or sets the list of switching steps for the active route + /// + [JsonProperty("steps")] + public List Steps { get; set; } + } + + /// + /// Represents a single switching step within a route + /// + public class RouteSwitchStepInfo + { + /// + /// Gets or sets the key of the switching device for the route step + /// + [JsonProperty("switchingDeviceKey", NullValueHandling = NullValueHandling.Ignore)] + public string SwitchingDeviceKey { get; set; } + + + /// + /// Gets or sets the key of the input port for the route step, if applicable + /// + [JsonProperty("inputPortKey", NullValueHandling = NullValueHandling.Ignore)] + public string InputPortKey { get; set; } + + /// + /// Gets or sets the key of the output port for the route step, if applicable + /// + [JsonProperty("outputPortKey", NullValueHandling = NullValueHandling.Ignore)] + public string OutputPortKey { get; set; } + } } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Web/RequestHandlers/LoginRequestHandler.cs b/src/PepperDash.Essentials.Core/Web/RequestHandlers/LoginRequestHandler.cs new file mode 100644 index 00000000..18e62d63 --- /dev/null +++ b/src/PepperDash.Essentials.Core/Web/RequestHandlers/LoginRequestHandler.cs @@ -0,0 +1,121 @@ + +using System; +using Crestron.SimplSharp.CrestronAuthentication; +using Crestron.SimplSharp.WebScripting; +using Newtonsoft.Json; +using PepperDash.Core.Web.RequestHandlers; + +namespace PepperDash.Essentials.Core.Web.RequestHandlers; + +/// +/// Represents a LoginRequestHandler +/// +public class LoginRequestHandler : WebApiBaseRequestHandler +{ + /// + /// Constructor + /// + /// + /// base(true) enables CORS support by default + /// + public LoginRequestHandler() + : base(true) + { + } + + /// + /// Handles POST method requests for user login and token generation + /// + /// The HTTP context for the request. + protected override void HandlePost(HttpCwsContext context) + { + try + { + if (context.Request.ContentLength < 0) + { + context.Response.StatusCode = 400; + context.Response.StatusDescription = "Bad Request"; + context.Response.End(); + + return; + } + + var data = context.Request.GetRequestBody(); + if (string.IsNullOrEmpty(data)) + { + context.Response.StatusCode = 400; + context.Response.StatusDescription = "Bad Request"; + context.Response.End(); + + return; + } + + var loginRequest = JsonConvert.DeserializeObject(data); + + if (loginRequest == null || string.IsNullOrEmpty(loginRequest.Username) || string.IsNullOrEmpty(loginRequest.Password)) + { + context.Response.StatusCode = 400; + context.Response.StatusDescription = "Bad Request"; + context.Response.End(); + + return; + } + + Authentication.UserToken token; + + try + { + token = Authentication.GetAuthenticationToken(loginRequest.Username, loginRequest.Password); + } + catch (ArgumentException) + { + context.Response.StatusCode = 401; + context.Response.StatusDescription = "Bad Request"; + context.Response.End(); + + return; + } + + if (!token.Valid) + { + context.Response.StatusCode = 401; + context.Response.StatusDescription = "Unauthorized"; + context.Response.End(); + + return; + } + + context.Response.StatusCode = 200; + context.Response.StatusDescription = "OK"; + context.Response.ContentType = "application/json"; + context.Response.ContentEncoding = System.Text.Encoding.UTF8; + context.Response.Write(JsonConvert.SerializeObject(new { Token = token }, Formatting.Indented), false); + context.Response.End(); + } + catch (System.Exception ex) + { + context.Response.StatusCode = 500; + context.Response.StatusDescription = "Internal Server Error"; + context.Response.ContentType = "application/json"; + context.Response.ContentEncoding = System.Text.Encoding.UTF8; + context.Response.Write(JsonConvert.SerializeObject(new { Error = ex.Message }, Formatting.Indented), false); + context.Response.End(); + } + } +} + +/// +/// Represents a LoginRequest +/// +public class LoginRequest +{ + /// + /// Gets or sets the username. + /// + public string Username { get; set; } + + /// + /// Gets or sets the password. + /// + public string Password { get; set; } +}