Resolve all high-severity architecture review findings
This commit is contained in:
@@ -156,7 +156,9 @@ public class ManagementActor : ReceiveActor
|
||||
private async Task<object> ProcessCommand(ManagementEnvelope envelope, AuthenticatedUser user)
|
||||
{
|
||||
using var scope = _serviceProvider.CreateScope();
|
||||
var result = await DispatchCommand(scope.ServiceProvider, envelope.Command, user, envelope.ClientIp);
|
||||
var result = IsConfigurationCommand(envelope.Command)
|
||||
? await ExecuteConfigurationCommandAsync(scope.ServiceProvider, envelope.Command, user, envelope.ClientIp)
|
||||
: await DispatchCommand(scope.ServiceProvider, envelope.Command, user, envelope.ClientIp);
|
||||
return new ManagementSuccess(envelope.CorrelationId, SerializeResult(result));
|
||||
}
|
||||
|
||||
@@ -187,6 +189,41 @@ public class ManagementActor : ReceiveActor
|
||||
return new ManagementError(correlationId, clientMessage, errorCode);
|
||||
}
|
||||
|
||||
/// <summary>Audited database-only application boundary shared by UI and management envelopes.</summary>
|
||||
public static async Task<object?> ExecuteConfigurationCommandAsync(IServiceProvider services, object command, AuthenticatedUser user, string? clientIp = null)
|
||||
{
|
||||
if (!IsConfigurationCommand(command)) throw new ArgumentException("Not a configuration command.", nameof(command));
|
||||
var required = GetRequiredRoles(command);
|
||||
if (required is null || !required.Any(r => user.Roles.Contains(r, StringComparer.OrdinalIgnoreCase)))
|
||||
throw new UnauthorizedAccessException("The current session is not authorized for this configuration change.");
|
||||
var transaction = services.GetRequiredService<IConfigurationCommandTransaction>();
|
||||
return await transaction.ExecuteAsync(sp => DispatchCommand(sp, command, user, clientIp));
|
||||
}
|
||||
|
||||
private static bool IsConfigurationCommand(object command) => command is
|
||||
CreateSiteCommand or UpdateSiteCommand or DeleteSiteCommand
|
||||
or CreateAreaCommand or UpdateAreaCommand or MoveAreaCommand or DeleteAreaCommand or SetInstanceAreaCommand
|
||||
or CreateDataConnectionCommand or UpdateDataConnectionCommand or DeleteDataConnectionCommand
|
||||
or CreateExternalSystemCommand or UpdateExternalSystemCommand or DeleteExternalSystemCommand
|
||||
or CreateDatabaseConnectionDefCommand or UpdateDatabaseConnectionDefCommand or DeleteDatabaseConnectionDefCommand
|
||||
or CreateApiMethodCommand or UpdateApiMethodCommand or DeleteApiMethodCommand
|
||||
or CreateRoleMappingCommand or UpdateRoleMappingCommand or DeleteRoleMappingCommand or AddScopeRuleCommand or DeleteScopeRuleCommand
|
||||
or CreateNotificationListCommand or UpdateNotificationListCommand or DeleteNotificationListCommand
|
||||
or UpdateSmtpConfigCommand or UpdateSmsConfigCommand
|
||||
or AddTemplateNativeAlarmSourceCommand or UpdateTemplateNativeAlarmSourceCommand or DeleteTemplateNativeAlarmSourceCommand
|
||||
or SetInstanceNativeAlarmSourceOverrideCommand or DeleteInstanceNativeAlarmSourceOverrideCommand
|
||||
or SetInstanceNativeAlarmSourceOverridesCommand or SetInstanceOverridesCommand
|
||||
or RenameNotificationListCommand or AddNotificationRecipientCommand or DeleteNotificationRecipientCommand
|
||||
or DeleteInstanceAttributeOverrideCommand;
|
||||
|
||||
private static Task AfterConfigurationCommit(IServiceProvider services, Func<Task> action)
|
||||
{
|
||||
var pending = services.GetService<IConfigurationCommitActions>();
|
||||
if (pending is null) return action();
|
||||
pending.Add(action);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
// Static readonly role sets for the authorization gate. Single-role arms
|
||||
// stay one-element arrays so every arm has the uniform any-of shape.
|
||||
private static readonly string[] AdminOnly = [Roles.Administrator];
|
||||
@@ -226,6 +263,8 @@ public class ManagementActor : ReceiveActor
|
||||
/// <returns>The set of roles that satisfy the authorization gate, or <c>null</c> if any authenticated user may issue the command.</returns>
|
||||
internal static IReadOnlyList<string>? GetRequiredRoles(object command) => command switch
|
||||
{
|
||||
RenameNotificationListCommand or AddNotificationRecipientCommand or DeleteNotificationRecipientCommand => DesignerOnly,
|
||||
DeleteInstanceAttributeOverrideCommand => DeployerOnly,
|
||||
// Administrator operations
|
||||
CreateSiteCommand or UpdateSiteCommand or DeleteSiteCommand
|
||||
or ListRoleMappingsCommand or CreateRoleMappingCommand
|
||||
@@ -335,11 +374,15 @@ public class ManagementActor : ReceiveActor
|
||||
_ => null
|
||||
};
|
||||
|
||||
private async Task<object?> DispatchCommand(
|
||||
private static async Task<object?> DispatchCommand(
|
||||
IServiceProvider sp, object command, AuthenticatedUser user, string? clientIp = null)
|
||||
{
|
||||
return command switch
|
||||
{
|
||||
RenameNotificationListCommand cmd => await HandleRenameNotificationList(sp, cmd, user.Username),
|
||||
AddNotificationRecipientCommand cmd => await HandleAddNotificationRecipient(sp, cmd, user.Username),
|
||||
DeleteNotificationRecipientCommand cmd => await HandleDeleteNotificationRecipient(sp, cmd, user.Username),
|
||||
DeleteInstanceAttributeOverrideCommand cmd => await HandleDeleteAttributeOverride(sp, cmd, user),
|
||||
// Templates
|
||||
ListTemplatesCommand cmd => await HandleListTemplates(sp, cmd),
|
||||
GetTemplateCommand cmd => await HandleGetTemplate(sp, cmd),
|
||||
@@ -403,10 +446,10 @@ public class ManagementActor : ReceiveActor
|
||||
UpdateSiteCommand cmd => await HandleUpdateSite(sp, cmd, user.Username),
|
||||
DeleteSiteCommand cmd => await HandleDeleteSite(sp, cmd, user.Username),
|
||||
ListAreasCommand cmd => await HandleListAreas(sp, cmd, user),
|
||||
CreateAreaCommand cmd => await HandleCreateArea(sp, cmd, user.Username),
|
||||
DeleteAreaCommand cmd => await HandleDeleteArea(sp, cmd, user.Username),
|
||||
UpdateAreaCommand cmd => await HandleUpdateArea(sp, cmd, user.Username),
|
||||
MoveAreaCommand cmd => await HandleMoveArea(sp, cmd, user.Username),
|
||||
CreateAreaCommand cmd => await HandleCreateArea(sp, cmd, user),
|
||||
DeleteAreaCommand cmd => await HandleDeleteArea(sp, cmd, user),
|
||||
UpdateAreaCommand cmd => await HandleUpdateArea(sp, cmd, user),
|
||||
MoveAreaCommand cmd => await HandleMoveArea(sp, cmd, user),
|
||||
|
||||
// Data Connections
|
||||
ListDataConnectionsCommand cmd => await HandleListDataConnections(sp, cmd, user),
|
||||
@@ -1845,7 +1888,7 @@ public class ManagementActor : ReceiveActor
|
||||
await repo.AddSiteAsync(site);
|
||||
await repo.SaveChangesAsync();
|
||||
var commService = sp.GetService<CommunicationService>();
|
||||
commService?.RefreshSiteAddresses();
|
||||
await AfterConfigurationCommit(sp, () => { commService?.RefreshSiteAddresses(); return Task.CompletedTask; });
|
||||
await AuditAsync(sp, user, "Create", "Site", site.Id.ToString(), site.Name, site);
|
||||
return site;
|
||||
}
|
||||
@@ -1864,7 +1907,7 @@ public class ManagementActor : ReceiveActor
|
||||
await repo.UpdateSiteAsync(site);
|
||||
await repo.SaveChangesAsync();
|
||||
var commService = sp.GetService<CommunicationService>();
|
||||
commService?.RefreshSiteAddresses();
|
||||
await AfterConfigurationCommit(sp, () => { commService?.RefreshSiteAddresses(); return Task.CompletedTask; });
|
||||
await AuditAsync(sp, user, "Update", "Site", site.Id.ToString(), site.Name, site);
|
||||
return site;
|
||||
}
|
||||
@@ -1880,7 +1923,7 @@ public class ManagementActor : ReceiveActor
|
||||
await repo.DeleteSiteAsync(cmd.SiteId);
|
||||
await repo.SaveChangesAsync();
|
||||
var commService = sp.GetService<CommunicationService>();
|
||||
commService?.RefreshSiteAddresses();
|
||||
await AfterConfigurationCommit(sp, () => { commService?.RefreshSiteAddresses(); return Task.CompletedTask; });
|
||||
|
||||
// Dispose the deleted site's cached gRPC channels — RemoveSiteAsync is the
|
||||
// factory's designed site-deletion disposal path and previously had no
|
||||
@@ -1891,7 +1934,7 @@ public class ManagementActor : ReceiveActor
|
||||
// the viewer linger (documented acceptance — see Component-Communication.md).
|
||||
var grpcFactory = sp.GetService<ZB.MOM.WW.ScadaBridge.Communication.Grpc.SiteStreamGrpcClientFactory>();
|
||||
if (grpcFactory is not null && site is not null)
|
||||
await grpcFactory.RemoveSiteAsync(site.SiteIdentifier);
|
||||
await AfterConfigurationCommit(sp, () => grpcFactory.RemoveSiteAsync(site.SiteIdentifier));
|
||||
|
||||
await AuditAsync(sp, user, "Delete", "Site", cmd.SiteId.ToString(), site?.Name ?? cmd.SiteId.ToString(), null);
|
||||
return true;
|
||||
@@ -1904,27 +1947,20 @@ public class ManagementActor : ReceiveActor
|
||||
return await repo.GetAreaTreeBySiteIdAsync(cmd.SiteId);
|
||||
}
|
||||
|
||||
private static async Task<object?> HandleCreateArea(IServiceProvider sp, CreateAreaCommand cmd, string user)
|
||||
private static async Task<object?> HandleCreateArea(IServiceProvider sp, CreateAreaCommand cmd, AuthenticatedUser user)
|
||||
{
|
||||
var repo = sp.GetRequiredService<ITemplateEngineRepository>();
|
||||
var area = new Area(cmd.Name)
|
||||
{
|
||||
SiteId = cmd.SiteId,
|
||||
ParentAreaId = cmd.ParentAreaId
|
||||
};
|
||||
await repo.AddAreaAsync(area);
|
||||
await repo.SaveChangesAsync();
|
||||
await AuditAsync(sp, user, "Create", "Area", area.Id.ToString(), area.Name, area);
|
||||
return area;
|
||||
EnforceSiteScope(user, cmd.SiteId);
|
||||
var result = await sp.GetRequiredService<AreaService>().CreateAreaAsync(cmd.Name, cmd.SiteId, cmd.ParentAreaId, user.Username);
|
||||
return result.IsSuccess ? result.Value : throw new ManagementCommandException(result.Error);
|
||||
}
|
||||
|
||||
private static async Task<object?> HandleDeleteArea(IServiceProvider sp, DeleteAreaCommand cmd, string user)
|
||||
private static async Task<object?> HandleDeleteArea(IServiceProvider sp, DeleteAreaCommand cmd, AuthenticatedUser user)
|
||||
{
|
||||
var repo = sp.GetRequiredService<ITemplateEngineRepository>();
|
||||
await repo.DeleteAreaAsync(cmd.AreaId);
|
||||
await repo.SaveChangesAsync();
|
||||
await AuditAsync(sp, user, "Delete", "Area", cmd.AreaId.ToString(), cmd.AreaId.ToString(), null);
|
||||
return true;
|
||||
var area = await sp.GetRequiredService<ITemplateEngineRepository>().GetAreaByIdAsync(cmd.AreaId)
|
||||
?? throw new ManagementCommandException($"Area with ID {cmd.AreaId} not found.");
|
||||
EnforceSiteScope(user, area.SiteId);
|
||||
var result = await sp.GetRequiredService<AreaService>().DeleteAreaAsync(cmd.AreaId, user.Username);
|
||||
return result.IsSuccess ? result.Value : throw new ManagementCommandException(result.Error);
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
@@ -2321,6 +2357,58 @@ public class ManagementActor : ReceiveActor
|
||||
// Notification handlers
|
||||
// ========================================================================
|
||||
|
||||
private static async Task<object?> HandleRenameNotificationList(IServiceProvider sp, RenameNotificationListCommand cmd, string user)
|
||||
{
|
||||
var repo = sp.GetRequiredService<INotificationRepository>();
|
||||
var list = await repo.GetNotificationListByIdAsync(cmd.NotificationListId)
|
||||
?? throw new ManagementCommandException("Notification list not found.");
|
||||
if (string.IsNullOrWhiteSpace(cmd.Name)) throw new ManagementCommandException("Name is required.");
|
||||
list.Name = cmd.Name.Trim();
|
||||
await repo.UpdateNotificationListAsync(list);
|
||||
await repo.SaveChangesAsync();
|
||||
await AuditAsync(sp, user, "Update", "NotificationList", list.Id.ToString(), list.Name, list);
|
||||
return list;
|
||||
}
|
||||
|
||||
private static async Task<object?> HandleAddNotificationRecipient(IServiceProvider sp, AddNotificationRecipientCommand cmd, string user)
|
||||
{
|
||||
var repo = sp.GetRequiredService<INotificationRepository>();
|
||||
var list = await repo.GetNotificationListByIdAsync(cmd.NotificationListId)
|
||||
?? throw new ManagementCommandException("Notification list not found.");
|
||||
if (list.Type == NotificationType.Sms ? string.IsNullOrWhiteSpace(cmd.PhoneNumber) : string.IsNullOrWhiteSpace(cmd.EmailAddress))
|
||||
throw new ManagementCommandException("A contact matching the list's channel is required.");
|
||||
var recipient = list.Type == NotificationType.Sms
|
||||
? NotificationRecipient.ForSms(cmd.Name, cmd.PhoneNumber!) : NotificationRecipient.ForEmail(cmd.Name, cmd.EmailAddress!);
|
||||
recipient.NotificationListId = list.Id;
|
||||
await repo.AddRecipientAsync(recipient);
|
||||
await repo.SaveChangesAsync();
|
||||
await AuditAsync(sp, user, "Create", "NotificationRecipient", recipient.Id.ToString(), recipient.Name, recipient);
|
||||
return recipient;
|
||||
}
|
||||
|
||||
private static async Task<object?> HandleDeleteNotificationRecipient(IServiceProvider sp, DeleteNotificationRecipientCommand cmd, string user)
|
||||
{
|
||||
var repo = sp.GetRequiredService<INotificationRepository>();
|
||||
var recipient = await repo.GetRecipientByIdAsync(cmd.RecipientId)
|
||||
?? throw new ManagementCommandException("Recipient not found.");
|
||||
await repo.DeleteRecipientAsync(cmd.RecipientId);
|
||||
await repo.SaveChangesAsync();
|
||||
await AuditAsync(sp, user, "Delete", "NotificationRecipient", cmd.RecipientId.ToString(), recipient.Name, null);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static async Task<object?> HandleDeleteAttributeOverride(IServiceProvider sp, DeleteInstanceAttributeOverrideCommand cmd, AuthenticatedUser user)
|
||||
{
|
||||
await EnforceSiteScopeForInstance(sp, user, cmd.InstanceId);
|
||||
var repo = sp.GetRequiredService<ITemplateEngineRepository>();
|
||||
var row = (await repo.GetOverridesByInstanceIdAsync(cmd.InstanceId)).FirstOrDefault(x => x.AttributeName == cmd.AttributeName);
|
||||
if (row is null) return true;
|
||||
await repo.DeleteInstanceAttributeOverrideAsync(row.Id);
|
||||
await repo.SaveChangesAsync();
|
||||
await AuditAsync(sp, user.Username, "Delete", "InstanceAttributeOverride", row.Id.ToString(), cmd.AttributeName, null);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static async Task<object?> HandleListNotificationLists(IServiceProvider sp)
|
||||
{
|
||||
var repo = sp.GetRequiredService<INotificationRepository>();
|
||||
@@ -2448,7 +2536,9 @@ public class ManagementActor : ReceiveActor
|
||||
private static async Task<object?> HandleUpdateSmtpConfig(IServiceProvider sp, UpdateSmtpConfigCommand cmd, string user)
|
||||
{
|
||||
var repo = sp.GetRequiredService<INotificationRepository>();
|
||||
var config = await repo.GetSmtpConfigurationByIdAsync(cmd.SmtpConfigId)
|
||||
var config = cmd is CreateSmtpConfigCommand
|
||||
? new Commons.Entities.Notifications.SmtpConfiguration(cmd.Server, cmd.AuthMode, cmd.FromAddress)
|
||||
: await repo.GetSmtpConfigurationByIdAsync(cmd.SmtpConfigId)
|
||||
?? throw new ManagementCommandException($"SmtpConfiguration with ID {cmd.SmtpConfigId} not found.");
|
||||
config.Host = cmd.Server;
|
||||
config.Port = cmd.Port;
|
||||
@@ -2466,13 +2556,14 @@ public class ManagementActor : ReceiveActor
|
||||
// one (null on every pre-EWS row, which means SMTP).
|
||||
if (cmd.Transport is not null) config.Transport = cmd.Transport;
|
||||
ValidateSmtpConfigTransportShape(config);
|
||||
await repo.UpdateSmtpConfigurationAsync(config);
|
||||
if (cmd is CreateSmtpConfigCommand) await repo.AddSmtpConfigurationAsync(config);
|
||||
else await repo.UpdateSmtpConfigurationAsync(config);
|
||||
await repo.SaveChangesAsync();
|
||||
// Audit the credential-free shape — the *fact of* the change
|
||||
// (and which non-secret fields hold) is observable; the secret value is
|
||||
// not persisted to the audit log where OperationalAuditRoles can read it.
|
||||
var publicShape = SmtpConfigPublicShape(config);
|
||||
await AuditAsync(sp, user, "Update", "SmtpConfiguration", config.Id.ToString(), config.Host, publicShape);
|
||||
await AuditAsync(sp, user, cmd is CreateSmtpConfigCommand ? "Create" : "Update", "SmtpConfiguration", config.Id.ToString(), config.Host, publicShape);
|
||||
return publicShape;
|
||||
}
|
||||
|
||||
@@ -2576,7 +2667,9 @@ public class ManagementActor : ReceiveActor
|
||||
{
|
||||
var repo = sp.GetRequiredService<INotificationRepository>();
|
||||
var configs = await repo.GetAllSmsConfigurationsAsync();
|
||||
var config = configs.FirstOrDefault(c => c.Id == cmd.SmsConfigId)
|
||||
var config = cmd is CreateSmsConfigCommand
|
||||
? new Commons.Entities.Notifications.SmsConfiguration(cmd.AccountSid, cmd.FromNumber)
|
||||
: configs.FirstOrDefault(c => c.Id == cmd.SmsConfigId)
|
||||
?? throw new ManagementCommandException($"SmsConfiguration with ID {cmd.SmsConfigId} not found.");
|
||||
// The AccountSid is interpolated un-escaped into the Twilio request URI
|
||||
// (SmsNotificationDeliveryAdapter). Reject any value that is not a well-formed
|
||||
@@ -2597,12 +2690,20 @@ public class ManagementActor : ReceiveActor
|
||||
// subsequent send. (Contrast SmtpConfiguration.Credentials, which MAY be cleared
|
||||
// when switching to anonymous SMTP, so that path keeps its null-only guard.)
|
||||
if (!string.IsNullOrWhiteSpace(cmd.AuthToken)) config.AuthToken = cmd.AuthToken;
|
||||
await repo.UpdateSmsConfigurationAsync(config);
|
||||
if (cmd.ConnectionTimeoutSeconds is { } timeout) config.ConnectionTimeoutSeconds = timeout;
|
||||
if (cmd.MaxRetries is { } retries) config.MaxRetries = retries;
|
||||
if (cmd.RetryDelay is { } retryDelay) config.RetryDelay = retryDelay;
|
||||
if (string.IsNullOrWhiteSpace(config.FromNumber) && string.IsNullOrWhiteSpace(config.MessagingServiceSid))
|
||||
throw new ManagementCommandException("A FromNumber or MessagingServiceSid is required.");
|
||||
if (cmd is CreateSmsConfigCommand && string.IsNullOrWhiteSpace(config.AuthToken))
|
||||
throw new ManagementCommandException("AuthToken is required.");
|
||||
if (cmd is CreateSmsConfigCommand) await repo.AddSmsConfigurationAsync(config);
|
||||
else await repo.UpdateSmsConfigurationAsync(config);
|
||||
await repo.SaveChangesAsync();
|
||||
// Audit the credential-free shape — the AuthToken secret is
|
||||
// not persisted to the audit log where OperationalAuditRoles can read it.
|
||||
var publicShape = SmsConfigPublicShape(config);
|
||||
await AuditAsync(sp, user, "Update", "SmsConfiguration", config.Id.ToString(), config.AccountSid, publicShape);
|
||||
await AuditAsync(sp, user, cmd is CreateSmsConfigCommand ? "Create" : "Update", "SmsConfiguration", config.Id.ToString(), config.AccountSid, publicShape);
|
||||
return publicShape;
|
||||
}
|
||||
|
||||
@@ -3414,6 +3515,8 @@ public class ManagementActor : ReceiveActor
|
||||
{
|
||||
var repo = sp.GetRequiredService<IExternalSystemRepository>();
|
||||
var def = new DatabaseConnectionDefinition(cmd.Name, cmd.ConnectionString);
|
||||
if (cmd.MaxRetries is { } retries) def.MaxRetries = retries;
|
||||
if (cmd.RetryDelay is { } retryDelay) def.RetryDelay = retryDelay;
|
||||
await repo.AddDatabaseConnectionAsync(def);
|
||||
await repo.SaveChangesAsync();
|
||||
await AuditAsync(sp, user, "Create", "DatabaseConnection", def.Id.ToString(), def.Name, new { def.Id, def.Name });
|
||||
@@ -3430,6 +3533,8 @@ public class ManagementActor : ReceiveActor
|
||||
// secret intact — the connection string is elided from List/Get, so a
|
||||
// round-trip must not be required to re-send it.
|
||||
if (cmd.ConnectionString is not null) def.ConnectionString = cmd.ConnectionString;
|
||||
if (cmd.MaxRetries is { } retries) def.MaxRetries = retries;
|
||||
if (cmd.RetryDelay is { } retryDelay) def.RetryDelay = retryDelay;
|
||||
await repo.UpdateDatabaseConnectionAsync(def);
|
||||
await repo.SaveChangesAsync();
|
||||
await AuditAsync(sp, user, "Update", "DatabaseConnection", def.Id.ToString(), def.Name, new { def.Id, def.Name });
|
||||
@@ -3511,7 +3616,7 @@ public class ManagementActor : ReceiveActor
|
||||
await repo.DeleteApiMethodAsync(cmd.ApiMethodId);
|
||||
await repo.SaveChangesAsync();
|
||||
if (method != null)
|
||||
sp.GetService<InboundAPI.InboundScriptExecutor>()?.InvalidateMethod(method.Name);
|
||||
await AfterConfigurationCommit(sp, () => { sp.GetService<InboundAPI.InboundScriptExecutor>()?.InvalidateMethod(method.Name); return Task.CompletedTask; });
|
||||
await AuditAsync(sp, user, "Delete", "ApiMethod", cmd.ApiMethodId.ToString(), method?.Name ?? cmd.ApiMethodId.ToString(), null);
|
||||
return true;
|
||||
}
|
||||
@@ -3532,7 +3637,13 @@ public class ManagementActor : ReceiveActor
|
||||
var executor = sp.GetService<InboundAPI.InboundScriptExecutor>();
|
||||
if (executor is null)
|
||||
return null;
|
||||
if (executor.CompileAndRegister(method, out var errors))
|
||||
var compiled = executor.ValidateCompilation(method, out var errors);
|
||||
var actions = sp.GetService<IConfigurationCommitActions>();
|
||||
if (actions is not null)
|
||||
actions.Add(() => { executor.CompileAndRegister(method); return Task.CompletedTask; });
|
||||
else
|
||||
executor.CompileAndRegister(method);
|
||||
if (compiled)
|
||||
return null;
|
||||
|
||||
var detail = string.Join("; ", errors);
|
||||
@@ -3620,16 +3731,13 @@ public class ManagementActor : ReceiveActor
|
||||
// Area update handler
|
||||
// ========================================================================
|
||||
|
||||
private static async Task<object?> HandleUpdateArea(IServiceProvider sp, UpdateAreaCommand cmd, string user)
|
||||
private static async Task<object?> HandleUpdateArea(IServiceProvider sp, UpdateAreaCommand cmd, AuthenticatedUser user)
|
||||
{
|
||||
var repo = sp.GetRequiredService<ITemplateEngineRepository>();
|
||||
var area = await repo.GetAreaByIdAsync(cmd.AreaId)
|
||||
var area = await sp.GetRequiredService<ITemplateEngineRepository>().GetAreaByIdAsync(cmd.AreaId)
|
||||
?? throw new ManagementCommandException($"Area with ID {cmd.AreaId} not found.");
|
||||
area.Name = cmd.Name;
|
||||
await repo.UpdateAreaAsync(area);
|
||||
await repo.SaveChangesAsync();
|
||||
await AuditAsync(sp, user, "Update", "Area", area.Id.ToString(), area.Name, area);
|
||||
return area;
|
||||
EnforceSiteScope(user, area.SiteId);
|
||||
var result = await sp.GetRequiredService<AreaService>().UpdateAreaAsync(cmd.AreaId, cmd.Name, user.Username);
|
||||
return result.IsSuccess ? result.Value : throw new ManagementCommandException(result.Error);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -3641,10 +3749,12 @@ public class ManagementActor : ReceiveActor
|
||||
/// response, matching the sibling folder/template move handlers. The service
|
||||
/// writes the "Move" audit row itself, so this handler does not audit again.
|
||||
/// </summary>
|
||||
private static async Task<object?> HandleMoveArea(IServiceProvider sp, MoveAreaCommand cmd, string user)
|
||||
private static async Task<object?> HandleMoveArea(IServiceProvider sp, MoveAreaCommand cmd, AuthenticatedUser user)
|
||||
{
|
||||
var svc = sp.GetRequiredService<AreaService>();
|
||||
var result = await svc.MoveAreaAsync(cmd.AreaId, cmd.NewParentAreaId, user);
|
||||
var area = await sp.GetRequiredService<ITemplateEngineRepository>().GetAreaByIdAsync(cmd.AreaId)
|
||||
?? throw new ManagementCommandException($"Area with ID {cmd.AreaId} not found.");
|
||||
EnforceSiteScope(user, area.SiteId);
|
||||
var result = await sp.GetRequiredService<AreaService>().MoveAreaAsync(cmd.AreaId, cmd.NewParentAreaId, user.Username);
|
||||
return result.IsSuccess ? result.Value : throw new ManagementCommandException(result.Error);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user