using HanaToolbox.Config;
using HanaToolbox.Logging;
using HanaToolbox.Services;
using HanaToolbox.Services.Interfaces;
using Spectre.Console;
namespace HanaToolbox.Tui;
///
/// Interactive Firewall TUI using Spectre.Console.
/// Mirrors the firewalld.sh interactive flow.
///
public sealed class FirewallTui(
FirewallService firewallService,
AppLogger _logger)
{
public async Task RunAsync(
FirewallConfig current, CancellationToken ct = default)
{
AnsiConsole.Clear();
AnsiConsole.Write(new Rule("[cyan]SAP B1 Firewall Configurator[/]").RuleStyle("cyan"));
AnsiConsole.WriteLine();
// Flush question
var flush = AnsiConsole.Confirm("Flush (remove) all current firewall rules before applying? [creates clean slate]",
defaultValue: current.FlushBeforeApply);
AnsiConsole.WriteLine();
// Per-service configuration
var entries = new List();
foreach (var svc in current.Services)
{
AnsiConsole.Write(new Rule($"[green]{svc.Name}[/] [grey]({string.Join(", ", svc.Ports)})[/]").RuleStyle("grey"));
var choice = AnsiConsole.Prompt(
new SelectionPrompt()
.Title("Access rule:")
.AddChoices("Allow from ANYWHERE (Public)", "Restrict to SPECIFIC IPs", "Skip / Block")
.HighlightStyle("cyan"));
var decision = choice switch
{
"Allow from ANYWHERE (Public)" => FirewallDecision.All,
"Restrict to SPECIFIC IPs" => FirewallDecision.Ip,
_ => FirewallDecision.Skip
};
var ips = new List();
if (decision == FirewallDecision.Ip)
{
var existing = string.Join(", ", svc.AllowedIps);
var ipInput = AnsiConsole.Prompt(
new TextPrompt("Enter IPs or subnets (comma-separated):")
.DefaultValue(existing.Length > 0 ? existing : "192.168.1.1")
.AllowEmpty());
ips = ipInput
.Split(',', StringSplitOptions.RemoveEmptyEntries)
.Select(i => i.Trim())
.Where(i => i.Length > 0)
.ToList();
}
entries.Add(new FirewallServiceEntry
{
Name = svc.Name,
Ports = svc.Ports,
Decision = decision,
AllowedIps = ips
});
AnsiConsole.WriteLine();
}
// Summary table
AnsiConsole.Write(new Rule("[yellow]Summary[/]").RuleStyle("yellow"));
var table = new Table()
.AddColumn("Service")
.AddColumn("Action")
.AddColumn("Details");
foreach (var e in entries)
{
var (action, details) = e.Decision switch
{
FirewallDecision.All => ("[red]Open Public[/]", "0.0.0.0/0"),
FirewallDecision.Ip => ("[green]Restricted[/]", string.Join(", ", e.AllowedIps)),
_ => ("[grey]Blocked/Skip[/]", "-")
};
table.AddRow(e.Name.Length > 35 ? e.Name[..35] : e.Name, action, details);
}
AnsiConsole.Write(table);
AnsiConsole.WriteLine();
if (!AnsiConsole.Confirm("Apply and save these rules?", defaultValue: true))
{
AnsiConsole.MarkupLine("[yellow]Aborted.[/]");
return null;
}
var updated = new FirewallConfig
{
FlushBeforeApply = flush,
Services = entries,
Enabled = current.Enabled,
ScheduleHour = current.ScheduleHour,
ScheduleMinute = current.ScheduleMinute
};
// Apply rules
await firewallService.ApplyAsync(updated, ct);
// Safety revert window
AnsiConsole.MarkupLine("[yellow]Rules applied. You have 15 seconds to confirm your connection still works.[/]");
AnsiConsole.MarkupLine("Press [cyan]ENTER[/] to keep changes permanently, or wait to auto-revert.");
var confirmed = await WaitForConfirmAsync(15, ct);
if (!confirmed)
{
AnsiConsole.MarkupLine("[red]Timeout — reverting firewall to permanent config...[/]");
await AnsiConsole.Status().StartAsync("Reverting...", async _ =>
{
await Task.Run(() =>
System.Diagnostics.Process.Start("/bin/bash", "-c firewall-cmd --reload")?.WaitForExit(), ct);
});
return null;
}
AnsiConsole.MarkupLine("[green]Changes confirmed and saved permanently.[/]");
return updated;
}
private static async Task WaitForConfirmAsync(int seconds, CancellationToken ct)
{
var cts = CancellationTokenSource.CreateLinkedTokenSource(ct);
cts.CancelAfter(TimeSpan.FromSeconds(seconds));
try
{
await Task.Run(() => Console.ReadLine(), cts.Token);
return true;
}
catch (OperationCanceledException)
{
return false;
}
}
}