diff --git a/src/ScadaLink.ManagementService/ManagementActor.cs b/src/ScadaLink.ManagementService/ManagementActor.cs new file mode 100644 index 0000000..e1c1702 --- /dev/null +++ b/src/ScadaLink.ManagementService/ManagementActor.cs @@ -0,0 +1,692 @@ +using System.Security.Cryptography; +using Akka.Actor; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using ScadaLink.Commons.Entities.ExternalSystems; +using ScadaLink.Commons.Entities.InboundApi; +using ScadaLink.Commons.Entities.Instances; +using ScadaLink.Commons.Entities.Notifications; +using ScadaLink.Commons.Entities.Security; +using ScadaLink.Commons.Entities.Sites; +using ScadaLink.Commons.Interfaces.Repositories; +using ScadaLink.Commons.Messages.Management; +using ScadaLink.DeploymentManager; +using ScadaLink.HealthMonitoring; +using ScadaLink.TemplateEngine; +using ScadaLink.TemplateEngine.Services; + +namespace ScadaLink.ManagementService; + +/// +/// Central actor that handles all management commands from the CLI (via ClusterClient). +/// Receives ManagementEnvelope messages, authorizes based on roles, then delegates to +/// the appropriate service or repository using scoped DI. +/// +public class ManagementActor : ReceiveActor +{ + private readonly IServiceProvider _serviceProvider; + private readonly ILogger _logger; + + public ManagementActor(IServiceProvider serviceProvider, ILogger logger) + { + _serviceProvider = serviceProvider; + _logger = logger; + Receive(HandleEnvelope); + } + + private void HandleEnvelope(ManagementEnvelope envelope) + { + var sender = Sender; + var correlationId = envelope.CorrelationId; + var user = envelope.User; + + // Check authorization + var requiredRole = GetRequiredRole(envelope.Command); + if (requiredRole != null && !user.Roles.Contains(requiredRole, StringComparer.OrdinalIgnoreCase)) + { + sender.Tell(new ManagementUnauthorized(correlationId, + $"Role '{requiredRole}' required for {envelope.Command.GetType().Name}")); + return; + } + + // Process command asynchronously with scoped DI + Task.Run(async () => + { + using var scope = _serviceProvider.CreateScope(); + try + { + var result = await DispatchCommand(scope.ServiceProvider, envelope.Command, user.Username); + sender.Tell(new ManagementSuccess(correlationId, result)); + } + catch (Exception ex) + { + _logger.LogError(ex, "Management command {Command} failed (CorrelationId={CorrelationId})", + envelope.Command.GetType().Name, correlationId); + sender.Tell(new ManagementError(correlationId, ex.Message, "COMMAND_FAILED")); + } + }); + } + + private static string? GetRequiredRole(object command) => command switch + { + // Admin operations + CreateSiteCommand or UpdateSiteCommand or DeleteSiteCommand + or CreateAreaCommand or DeleteAreaCommand + or ListRoleMappingsCommand or CreateRoleMappingCommand + or UpdateRoleMappingCommand or DeleteRoleMappingCommand + or ListApiKeysCommand or CreateApiKeyCommand or DeleteApiKeyCommand => "Admin", + + // Design operations + CreateTemplateCommand or UpdateTemplateCommand or DeleteTemplateCommand + or ValidateTemplateCommand + or CreateExternalSystemCommand or UpdateExternalSystemCommand + or DeleteExternalSystemCommand + or CreateNotificationListCommand or UpdateNotificationListCommand + or DeleteNotificationListCommand + or UpdateSmtpConfigCommand + or CreateDataConnectionCommand or UpdateDataConnectionCommand + or DeleteDataConnectionCommand + or AssignDataConnectionToSiteCommand + or UnassignDataConnectionFromSiteCommand => "Design", + + // Deployment operations + CreateInstanceCommand or MgmtDeployInstanceCommand or MgmtEnableInstanceCommand + or MgmtDisableInstanceCommand or MgmtDeleteInstanceCommand + or SetConnectionBindingsCommand + or MgmtDeployArtifactsCommand => "Deployment", + + // Read-only queries -- any authenticated user + _ => null + }; + + private async Task DispatchCommand(IServiceProvider sp, object command, string user) + { + return command switch + { + // Templates + ListTemplatesCommand => await HandleListTemplates(sp), + GetTemplateCommand cmd => await HandleGetTemplate(sp, cmd), + CreateTemplateCommand cmd => await HandleCreateTemplate(sp, cmd, user), + UpdateTemplateCommand cmd => await HandleUpdateTemplate(sp, cmd, user), + DeleteTemplateCommand cmd => await HandleDeleteTemplate(sp, cmd, user), + ValidateTemplateCommand cmd => await HandleValidateTemplate(sp, cmd), + + // Instances + ListInstancesCommand cmd => await HandleListInstances(sp, cmd), + GetInstanceCommand cmd => await HandleGetInstance(sp, cmd), + CreateInstanceCommand cmd => await HandleCreateInstance(sp, cmd, user), + MgmtDeployInstanceCommand cmd => await HandleDeployInstance(sp, cmd, user), + MgmtEnableInstanceCommand cmd => await HandleEnableInstance(sp, cmd, user), + MgmtDisableInstanceCommand cmd => await HandleDisableInstance(sp, cmd, user), + MgmtDeleteInstanceCommand cmd => await HandleDeleteInstance(sp, cmd, user), + SetConnectionBindingsCommand cmd => await HandleSetConnectionBindings(sp, cmd, user), + + // Sites + ListSitesCommand => await HandleListSites(sp), + GetSiteCommand cmd => await HandleGetSite(sp, cmd), + CreateSiteCommand cmd => await HandleCreateSite(sp, cmd), + UpdateSiteCommand cmd => await HandleUpdateSite(sp, cmd), + DeleteSiteCommand cmd => await HandleDeleteSite(sp, cmd), + ListAreasCommand cmd => await HandleListAreas(sp, cmd), + CreateAreaCommand cmd => await HandleCreateArea(sp, cmd), + DeleteAreaCommand cmd => await HandleDeleteArea(sp, cmd), + + // Data Connections + ListDataConnectionsCommand => await HandleListDataConnections(sp), + GetDataConnectionCommand cmd => await HandleGetDataConnection(sp, cmd), + CreateDataConnectionCommand cmd => await HandleCreateDataConnection(sp, cmd), + UpdateDataConnectionCommand cmd => await HandleUpdateDataConnection(sp, cmd), + DeleteDataConnectionCommand cmd => await HandleDeleteDataConnection(sp, cmd), + AssignDataConnectionToSiteCommand cmd => await HandleAssignDataConnectionToSite(sp, cmd), + UnassignDataConnectionFromSiteCommand cmd => await HandleUnassignDataConnectionFromSite(sp, cmd), + + // External Systems + ListExternalSystemsCommand => await HandleListExternalSystems(sp), + GetExternalSystemCommand cmd => await HandleGetExternalSystem(sp, cmd), + CreateExternalSystemCommand cmd => await HandleCreateExternalSystem(sp, cmd), + UpdateExternalSystemCommand cmd => await HandleUpdateExternalSystem(sp, cmd), + DeleteExternalSystemCommand cmd => await HandleDeleteExternalSystem(sp, cmd), + + // Notification Lists + ListNotificationListsCommand => await HandleListNotificationLists(sp), + GetNotificationListCommand cmd => await HandleGetNotificationList(sp, cmd), + CreateNotificationListCommand cmd => await HandleCreateNotificationList(sp, cmd), + UpdateNotificationListCommand cmd => await HandleUpdateNotificationList(sp, cmd), + DeleteNotificationListCommand cmd => await HandleDeleteNotificationList(sp, cmd), + ListSmtpConfigsCommand => await HandleListSmtpConfigs(sp), + UpdateSmtpConfigCommand cmd => await HandleUpdateSmtpConfig(sp, cmd), + + // Security + ListRoleMappingsCommand => await HandleListRoleMappings(sp), + CreateRoleMappingCommand cmd => await HandleCreateRoleMapping(sp, cmd), + UpdateRoleMappingCommand cmd => await HandleUpdateRoleMapping(sp, cmd), + DeleteRoleMappingCommand cmd => await HandleDeleteRoleMapping(sp, cmd), + ListApiKeysCommand => await HandleListApiKeys(sp), + CreateApiKeyCommand cmd => await HandleCreateApiKey(sp, cmd), + DeleteApiKeyCommand cmd => await HandleDeleteApiKey(sp, cmd), + + // Deployments + MgmtDeployArtifactsCommand cmd => await HandleDeployArtifacts(sp, cmd, user), + QueryDeploymentsCommand cmd => await HandleQueryDeployments(sp, cmd), + + // Audit Log + QueryAuditLogCommand cmd => await HandleQueryAuditLog(sp, cmd), + + // Health + GetHealthSummaryCommand => HandleGetHealthSummary(sp), + GetSiteHealthCommand cmd => HandleGetSiteHealth(sp, cmd), + + _ => throw new NotSupportedException($"Unknown command type: {command.GetType().Name}") + }; + } + + // ======================================================================== + // Template handlers + // ======================================================================== + + private static async Task HandleListTemplates(IServiceProvider sp) + { + var repo = sp.GetRequiredService(); + return await repo.GetAllTemplatesAsync(); + } + + private static async Task HandleGetTemplate(IServiceProvider sp, GetTemplateCommand cmd) + { + var repo = sp.GetRequiredService(); + return await repo.GetTemplateWithChildrenAsync(cmd.TemplateId); + } + + private static async Task HandleCreateTemplate(IServiceProvider sp, CreateTemplateCommand cmd, string user) + { + var svc = sp.GetRequiredService(); + var result = await svc.CreateTemplateAsync(cmd.Name, cmd.Description, cmd.ParentTemplateId, user); + return result.IsSuccess + ? result.Value + : throw new InvalidOperationException(result.Error); + } + + private static async Task HandleUpdateTemplate(IServiceProvider sp, UpdateTemplateCommand cmd, string user) + { + var svc = sp.GetRequiredService(); + var result = await svc.UpdateTemplateAsync(cmd.TemplateId, cmd.Name, cmd.Description, cmd.ParentTemplateId, user); + return result.IsSuccess + ? result.Value + : throw new InvalidOperationException(result.Error); + } + + private static async Task HandleDeleteTemplate(IServiceProvider sp, DeleteTemplateCommand cmd, string user) + { + var svc = sp.GetRequiredService(); + var result = await svc.DeleteTemplateAsync(cmd.TemplateId, user); + return result.IsSuccess + ? result.Value + : throw new InvalidOperationException(result.Error); + } + + private static async Task HandleValidateTemplate(IServiceProvider sp, ValidateTemplateCommand cmd) + { + var svc = sp.GetRequiredService(); + return await svc.DetectCollisionsAsync(cmd.TemplateId); + } + + // ======================================================================== + // Instance handlers + // ======================================================================== + + private static async Task HandleListInstances(IServiceProvider sp, ListInstancesCommand cmd) + { + var repo = sp.GetRequiredService(); + return await repo.GetInstancesFilteredAsync(cmd.SiteId, cmd.TemplateId, cmd.SearchTerm); + } + + private static async Task HandleGetInstance(IServiceProvider sp, GetInstanceCommand cmd) + { + var repo = sp.GetRequiredService(); + return await repo.GetInstanceByIdAsync(cmd.InstanceId); + } + + private static async Task HandleCreateInstance(IServiceProvider sp, CreateInstanceCommand cmd, string user) + { + var svc = sp.GetRequiredService(); + var result = await svc.CreateInstanceAsync(cmd.UniqueName, cmd.TemplateId, cmd.SiteId, cmd.AreaId, user); + return result.IsSuccess + ? result.Value + : throw new InvalidOperationException(result.Error); + } + + private static async Task HandleDeployInstance(IServiceProvider sp, MgmtDeployInstanceCommand cmd, string user) + { + var svc = sp.GetRequiredService(); + var result = await svc.DeployInstanceAsync(cmd.InstanceId, user); + return result.IsSuccess + ? result.Value + : throw new InvalidOperationException(result.Error); + } + + private static async Task HandleEnableInstance(IServiceProvider sp, MgmtEnableInstanceCommand cmd, string user) + { + var svc = sp.GetRequiredService(); + var result = await svc.EnableInstanceAsync(cmd.InstanceId, user); + return result.IsSuccess + ? result.Value + : throw new InvalidOperationException(result.Error); + } + + private static async Task HandleDisableInstance(IServiceProvider sp, MgmtDisableInstanceCommand cmd, string user) + { + var svc = sp.GetRequiredService(); + var result = await svc.DisableInstanceAsync(cmd.InstanceId, user); + return result.IsSuccess + ? result.Value + : throw new InvalidOperationException(result.Error); + } + + private static async Task HandleDeleteInstance(IServiceProvider sp, MgmtDeleteInstanceCommand cmd, string user) + { + var svc = sp.GetRequiredService(); + var result = await svc.DeleteInstanceAsync(cmd.InstanceId, user); + return result.IsSuccess + ? result.Value + : throw new InvalidOperationException(result.Error); + } + + private static async Task HandleSetConnectionBindings(IServiceProvider sp, SetConnectionBindingsCommand cmd, string user) + { + var svc = sp.GetRequiredService(); + var result = await svc.SetConnectionBindingsAsync(cmd.InstanceId, cmd.Bindings, user); + return result.IsSuccess + ? result.Value + : throw new InvalidOperationException(result.Error); + } + + // ======================================================================== + // Site handlers + // ======================================================================== + + private static async Task HandleListSites(IServiceProvider sp) + { + var repo = sp.GetRequiredService(); + return await repo.GetAllSitesAsync(); + } + + private static async Task HandleGetSite(IServiceProvider sp, GetSiteCommand cmd) + { + var repo = sp.GetRequiredService(); + return await repo.GetSiteByIdAsync(cmd.SiteId); + } + + private static async Task HandleCreateSite(IServiceProvider sp, CreateSiteCommand cmd) + { + var repo = sp.GetRequiredService(); + var site = new Site(cmd.Name, cmd.SiteIdentifier) { Description = cmd.Description }; + await repo.AddSiteAsync(site); + await repo.SaveChangesAsync(); + return site; + } + + private static async Task HandleUpdateSite(IServiceProvider sp, UpdateSiteCommand cmd) + { + var repo = sp.GetRequiredService(); + var site = await repo.GetSiteByIdAsync(cmd.SiteId) + ?? throw new InvalidOperationException($"Site with ID {cmd.SiteId} not found."); + site.Name = cmd.Name; + site.Description = cmd.Description; + await repo.UpdateSiteAsync(site); + await repo.SaveChangesAsync(); + return site; + } + + private static async Task HandleDeleteSite(IServiceProvider sp, DeleteSiteCommand cmd) + { + var repo = sp.GetRequiredService(); + // Check for instances referencing this site + var instances = await repo.GetInstancesBySiteIdAsync(cmd.SiteId); + if (instances.Count > 0) + throw new InvalidOperationException( + $"Cannot delete site {cmd.SiteId}: it has {instances.Count} instance(s)."); + await repo.DeleteSiteAsync(cmd.SiteId); + await repo.SaveChangesAsync(); + return true; + } + + private static async Task HandleListAreas(IServiceProvider sp, ListAreasCommand cmd) + { + var repo = sp.GetRequiredService(); + return await repo.GetAreaTreeBySiteIdAsync(cmd.SiteId); + } + + private static async Task HandleCreateArea(IServiceProvider sp, CreateAreaCommand cmd) + { + var repo = sp.GetRequiredService(); + var area = new Area(cmd.Name) + { + SiteId = cmd.SiteId, + ParentAreaId = cmd.ParentAreaId + }; + await repo.AddAreaAsync(area); + await repo.SaveChangesAsync(); + return area; + } + + private static async Task HandleDeleteArea(IServiceProvider sp, DeleteAreaCommand cmd) + { + var repo = sp.GetRequiredService(); + await repo.DeleteAreaAsync(cmd.AreaId); + await repo.SaveChangesAsync(); + return true; + } + + // ======================================================================== + // Data Connection handlers + // ======================================================================== + + private static async Task HandleListDataConnections(IServiceProvider sp) + { + var repo = sp.GetRequiredService(); + return await repo.GetAllDataConnectionsAsync(); + } + + private static async Task HandleGetDataConnection(IServiceProvider sp, GetDataConnectionCommand cmd) + { + var repo = sp.GetRequiredService(); + return await repo.GetDataConnectionByIdAsync(cmd.DataConnectionId); + } + + private static async Task HandleCreateDataConnection(IServiceProvider sp, CreateDataConnectionCommand cmd) + { + var repo = sp.GetRequiredService(); + var conn = new DataConnection(cmd.Name, cmd.Protocol) { Configuration = cmd.Configuration }; + await repo.AddDataConnectionAsync(conn); + await repo.SaveChangesAsync(); + return conn; + } + + private static async Task HandleUpdateDataConnection(IServiceProvider sp, UpdateDataConnectionCommand cmd) + { + var repo = sp.GetRequiredService(); + var conn = await repo.GetDataConnectionByIdAsync(cmd.DataConnectionId) + ?? throw new InvalidOperationException($"DataConnection with ID {cmd.DataConnectionId} not found."); + conn.Name = cmd.Name; + conn.Protocol = cmd.Protocol; + conn.Configuration = cmd.Configuration; + await repo.UpdateDataConnectionAsync(conn); + await repo.SaveChangesAsync(); + return conn; + } + + private static async Task HandleDeleteDataConnection(IServiceProvider sp, DeleteDataConnectionCommand cmd) + { + var repo = sp.GetRequiredService(); + await repo.DeleteDataConnectionAsync(cmd.DataConnectionId); + await repo.SaveChangesAsync(); + return true; + } + + private static async Task HandleAssignDataConnectionToSite(IServiceProvider sp, AssignDataConnectionToSiteCommand cmd) + { + var repo = sp.GetRequiredService(); + var assignment = new SiteDataConnectionAssignment + { + SiteId = cmd.SiteId, + DataConnectionId = cmd.DataConnectionId + }; + await repo.AddSiteDataConnectionAssignmentAsync(assignment); + await repo.SaveChangesAsync(); + return assignment; + } + + private static async Task HandleUnassignDataConnectionFromSite(IServiceProvider sp, UnassignDataConnectionFromSiteCommand cmd) + { + var repo = sp.GetRequiredService(); + await repo.DeleteSiteDataConnectionAssignmentAsync(cmd.AssignmentId); + await repo.SaveChangesAsync(); + return true; + } + + // ======================================================================== + // External System handlers + // ======================================================================== + + private static async Task HandleListExternalSystems(IServiceProvider sp) + { + var repo = sp.GetRequiredService(); + return await repo.GetAllExternalSystemsAsync(); + } + + private static async Task HandleGetExternalSystem(IServiceProvider sp, GetExternalSystemCommand cmd) + { + var repo = sp.GetRequiredService(); + return await repo.GetExternalSystemByIdAsync(cmd.ExternalSystemId); + } + + private static async Task HandleCreateExternalSystem(IServiceProvider sp, CreateExternalSystemCommand cmd) + { + var repo = sp.GetRequiredService(); + var def = new ExternalSystemDefinition(cmd.Name, cmd.EndpointUrl, cmd.AuthType) + { + AuthConfiguration = cmd.AuthConfiguration + }; + await repo.AddExternalSystemAsync(def); + await repo.SaveChangesAsync(); + return def; + } + + private static async Task HandleUpdateExternalSystem(IServiceProvider sp, UpdateExternalSystemCommand cmd) + { + var repo = sp.GetRequiredService(); + var def = await repo.GetExternalSystemByIdAsync(cmd.ExternalSystemId) + ?? throw new InvalidOperationException($"ExternalSystem with ID {cmd.ExternalSystemId} not found."); + def.Name = cmd.Name; + def.EndpointUrl = cmd.EndpointUrl; + def.AuthType = cmd.AuthType; + def.AuthConfiguration = cmd.AuthConfiguration; + await repo.UpdateExternalSystemAsync(def); + await repo.SaveChangesAsync(); + return def; + } + + private static async Task HandleDeleteExternalSystem(IServiceProvider sp, DeleteExternalSystemCommand cmd) + { + var repo = sp.GetRequiredService(); + await repo.DeleteExternalSystemAsync(cmd.ExternalSystemId); + await repo.SaveChangesAsync(); + return true; + } + + // ======================================================================== + // Notification handlers + // ======================================================================== + + private static async Task HandleListNotificationLists(IServiceProvider sp) + { + var repo = sp.GetRequiredService(); + return await repo.GetAllNotificationListsAsync(); + } + + private static async Task HandleGetNotificationList(IServiceProvider sp, GetNotificationListCommand cmd) + { + var repo = sp.GetRequiredService(); + return await repo.GetNotificationListByIdAsync(cmd.NotificationListId); + } + + private static async Task HandleCreateNotificationList(IServiceProvider sp, CreateNotificationListCommand cmd) + { + var repo = sp.GetRequiredService(); + var list = new NotificationList(cmd.Name); + foreach (var email in cmd.RecipientEmails) + { + list.Recipients.Add(new NotificationRecipient(email, email)); + } + await repo.AddNotificationListAsync(list); + await repo.SaveChangesAsync(); + return list; + } + + private static async Task HandleUpdateNotificationList(IServiceProvider sp, UpdateNotificationListCommand cmd) + { + var repo = sp.GetRequiredService(); + var list = await repo.GetNotificationListByIdAsync(cmd.NotificationListId) + ?? throw new InvalidOperationException($"NotificationList with ID {cmd.NotificationListId} not found."); + list.Name = cmd.Name; + + // Remove existing recipients and re-add + var existingRecipients = await repo.GetRecipientsByListIdAsync(cmd.NotificationListId); + foreach (var r in existingRecipients) + { + await repo.DeleteRecipientAsync(r.Id); + } + + foreach (var email in cmd.RecipientEmails) + { + await repo.AddRecipientAsync(new NotificationRecipient(email, email) + { + NotificationListId = cmd.NotificationListId + }); + } + + await repo.UpdateNotificationListAsync(list); + await repo.SaveChangesAsync(); + return list; + } + + private static async Task HandleDeleteNotificationList(IServiceProvider sp, DeleteNotificationListCommand cmd) + { + var repo = sp.GetRequiredService(); + await repo.DeleteNotificationListAsync(cmd.NotificationListId); + await repo.SaveChangesAsync(); + return true; + } + + private static async Task HandleListSmtpConfigs(IServiceProvider sp) + { + var repo = sp.GetRequiredService(); + return await repo.GetAllSmtpConfigurationsAsync(); + } + + private static async Task HandleUpdateSmtpConfig(IServiceProvider sp, UpdateSmtpConfigCommand cmd) + { + var repo = sp.GetRequiredService(); + var config = await repo.GetSmtpConfigurationByIdAsync(cmd.SmtpConfigId) + ?? throw new InvalidOperationException($"SmtpConfiguration with ID {cmd.SmtpConfigId} not found."); + config.Host = cmd.Server; + config.Port = cmd.Port; + config.AuthType = cmd.AuthMode; + config.FromAddress = cmd.FromAddress; + await repo.UpdateSmtpConfigurationAsync(config); + await repo.SaveChangesAsync(); + return config; + } + + // ======================================================================== + // Security handlers + // ======================================================================== + + private static async Task HandleListRoleMappings(IServiceProvider sp) + { + var repo = sp.GetRequiredService(); + return await repo.GetAllMappingsAsync(); + } + + private static async Task HandleCreateRoleMapping(IServiceProvider sp, CreateRoleMappingCommand cmd) + { + var repo = sp.GetRequiredService(); + var mapping = new LdapGroupMapping(cmd.LdapGroupName, cmd.Role); + await repo.AddMappingAsync(mapping); + await repo.SaveChangesAsync(); + return mapping; + } + + private static async Task HandleUpdateRoleMapping(IServiceProvider sp, UpdateRoleMappingCommand cmd) + { + var repo = sp.GetRequiredService(); + var mapping = await repo.GetMappingByIdAsync(cmd.MappingId) + ?? throw new InvalidOperationException($"RoleMapping with ID {cmd.MappingId} not found."); + mapping.LdapGroupName = cmd.LdapGroupName; + mapping.Role = cmd.Role; + await repo.UpdateMappingAsync(mapping); + await repo.SaveChangesAsync(); + return mapping; + } + + private static async Task HandleDeleteRoleMapping(IServiceProvider sp, DeleteRoleMappingCommand cmd) + { + var repo = sp.GetRequiredService(); + await repo.DeleteMappingAsync(cmd.MappingId); + await repo.SaveChangesAsync(); + return true; + } + + private static async Task HandleListApiKeys(IServiceProvider sp) + { + var repo = sp.GetRequiredService(); + return await repo.GetAllApiKeysAsync(); + } + + private static async Task HandleCreateApiKey(IServiceProvider sp, CreateApiKeyCommand cmd) + { + var repo = sp.GetRequiredService(); + var keyValue = Convert.ToBase64String(RandomNumberGenerator.GetBytes(32)); + var apiKey = new ApiKey(cmd.Name, keyValue) { IsEnabled = true }; + await repo.AddApiKeyAsync(apiKey); + await repo.SaveChangesAsync(); + return apiKey; + } + + private static async Task HandleDeleteApiKey(IServiceProvider sp, DeleteApiKeyCommand cmd) + { + var repo = sp.GetRequiredService(); + await repo.DeleteApiKeyAsync(cmd.ApiKeyId); + await repo.SaveChangesAsync(); + return true; + } + + // ======================================================================== + // Deployment handlers + // ======================================================================== + + private static async Task HandleDeployArtifacts(IServiceProvider sp, MgmtDeployArtifactsCommand cmd, string user) + { + var svc = sp.GetRequiredService(); + var command = await svc.BuildDeployArtifactsCommandAsync(); + var result = await svc.DeployToAllSitesAsync(command, user); + return result.IsSuccess + ? result.Value + : throw new InvalidOperationException(result.Error); + } + + private static async Task HandleQueryDeployments(IServiceProvider sp, QueryDeploymentsCommand cmd) + { + var repo = sp.GetRequiredService(); + if (cmd.InstanceId.HasValue) + return await repo.GetDeploymentsByInstanceIdAsync(cmd.InstanceId.Value); + return await repo.GetAllDeploymentRecordsAsync(); + } + + // ======================================================================== + // Audit Log handler + // ======================================================================== + + private static async Task HandleQueryAuditLog(IServiceProvider sp, QueryAuditLogCommand cmd) + { + var repo = sp.GetRequiredService(); + return await repo.GetAuditLogEntriesAsync( + cmd.User, cmd.EntityType, cmd.Action, cmd.From, cmd.To, + page: cmd.Page, pageSize: cmd.PageSize); + } + + // ======================================================================== + // Health handlers + // ======================================================================== + + private static object? HandleGetHealthSummary(IServiceProvider sp) + { + var aggregator = sp.GetRequiredService(); + return aggregator.GetAllSiteStates(); + } + + private static object? HandleGetSiteHealth(IServiceProvider sp, GetSiteHealthCommand cmd) + { + var aggregator = sp.GetRequiredService(); + return aggregator.GetSiteState(cmd.SiteIdentifier); + } +}