using System.Diagnostics; namespace LmeLicenseManager; internal sealed class MainForm : Form { private readonly LicenseRepository _repository; private readonly TextBox _firstName = new(); private readonly TextBox _lastName = new(); private readonly TextBox _email = new(); private readonly DateTimePicker _expiresAt = new(); private readonly TextBox _notes = new(); private readonly TextBox _licenseCode = new(); private readonly DataGridView _grid = new(); private readonly Label _status = new(); private IReadOnlyList _licenses = []; public MainForm(LicenseRepository repository) { _repository = repository; Text = "LME Pro - Gestore licenze manuali"; MinimumSize = new Size(980, 720); StartPosition = FormStartPosition.CenterScreen; Font = new Font("Segoe UI", 9F); var root = new TableLayoutPanel { Dock = DockStyle.Fill, ColumnCount = 1, RowCount = 5, Padding = new Padding(18) }; root.RowStyles.Add(new RowStyle(SizeType.AutoSize)); root.RowStyles.Add(new RowStyle(SizeType.AutoSize)); root.RowStyles.Add(new RowStyle(SizeType.AutoSize)); root.RowStyles.Add(new RowStyle(SizeType.Percent, 100)); root.RowStyles.Add(new RowStyle(SizeType.AutoSize)); Controls.Add(root); root.Controls.Add(BuildHeader(), 0, 0); root.Controls.Add(BuildIssueForm(), 0, 1); root.Controls.Add(BuildCodePanel(), 0, 2); root.Controls.Add(BuildGrid(), 0, 3); _status.AutoSize = true; _status.Padding = new Padding(0, 8, 0, 0); _status.ForeColor = Color.FromArgb(60, 77, 88); root.Controls.Add(_status, 0, 4); RefreshLicenses(); } private Control BuildHeader() { var panel = new TableLayoutPanel { Dock = DockStyle.Top, AutoSize = true, ColumnCount = 2 }; panel.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100)); panel.ColumnStyles.Add(new ColumnStyle(SizeType.AutoSize)); var text = new Label { AutoSize = true, Text = "Licenze manuali LME Pro\r\nEmetti codici cifrati e conserva titolari e scadenze nel registro locale.", Font = new Font("Segoe UI", 12F, FontStyle.Bold), Padding = new Padding(0, 0, 0, 14) }; panel.Controls.Add(text, 0, 0); var commands = new FlowLayoutPanel { AutoSize = true, FlowDirection = FlowDirection.LeftToRight, Margin = new Padding(8, 0, 0, 12) }; var openFolder = new Button { Text = "Apri archivio", AutoSize = true }; openFolder.Click += (_, _) => Process.Start(new ProcessStartInfo("explorer.exe", _repository.StoragePath) { UseShellExecute = true }); var exportKey = new Button { Text = "Backup chiave", AutoSize = true }; exportKey.Click += (_, _) => ExportSigningKey(); var importKey = new Button { Text = "Ripristina chiave", AutoSize = true }; importKey.Click += (_, _) => ImportSigningKey(); commands.Controls.Add(openFolder); commands.Controls.Add(exportKey); commands.Controls.Add(importKey); panel.Controls.Add(commands, 1, 0); return panel; } private Control BuildIssueForm() { var group = new GroupBox { Text = "Nuova licenza", Dock = DockStyle.Top, AutoSize = true, Padding = new Padding(12) }; var form = new TableLayoutPanel { Dock = DockStyle.Top, AutoSize = true, ColumnCount = 4, RowCount = 3 }; form.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 25)); form.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 25)); form.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 30)); form.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 20)); AddLabeledControl(form, "Nome", _firstName, 0, 0); AddLabeledControl(form, "Cognome", _lastName, 1, 0); AddLabeledControl(form, "Email", _email, 2, 0); _expiresAt.Format = DateTimePickerFormat.Custom; _expiresAt.CustomFormat = "dd/MM/yyyy HH:mm"; _expiresAt.Value = DateTime.Now.AddYears(1); AddLabeledControl(form, "Scadenza", _expiresAt, 3, 0); _notes.PlaceholderText = "Riferimento fattura, cliente, note interne..."; AddLabeledControl(form, "Note", _notes, 0, 1, 3); var issue = new Button { Text = "Emetti licenza", Dock = DockStyle.Fill, Height = 36, Margin = new Padding(6, 23, 3, 4), BackColor = Color.FromArgb(7, 94, 84), ForeColor = Color.White, FlatStyle = FlatStyle.Flat }; issue.Click += (_, _) => IssueLicense(); form.Controls.Add(issue, 3, 1); var note = new Label { AutoSize = true, Text = "La revoca registrata qui non disattiva un codice già consegnato e offline: la disattivazione effettiva avviene alla scadenza.", ForeColor = Color.FromArgb(90, 103, 111), Padding = new Padding(3, 8, 3, 2) }; form.Controls.Add(note, 0, 2); form.SetColumnSpan(note, 4); group.Controls.Add(form); return group; } private Control BuildCodePanel() { var group = new GroupBox { Text = "Codice da consegnare", Dock = DockStyle.Top, Height = 122, Padding = new Padding(12) }; var layout = new TableLayoutPanel { Dock = DockStyle.Fill, ColumnCount = 2 }; layout.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100)); layout.ColumnStyles.Add(new ColumnStyle(SizeType.AutoSize)); _licenseCode.Multiline = true; _licenseCode.ReadOnly = true; _licenseCode.ScrollBars = ScrollBars.Vertical; _licenseCode.Dock = DockStyle.Fill; _licenseCode.Font = new Font("Consolas", 8.5F); layout.Controls.Add(_licenseCode, 0, 0); var copy = new Button { Text = "Copia codice", AutoSize = true, Dock = DockStyle.Top, Margin = new Padding(8, 0, 0, 0) }; copy.Click += (_, _) => CopyCurrentCode(); layout.Controls.Add(copy, 1, 0); group.Controls.Add(layout); return group; } private Control BuildGrid() { var panel = new TableLayoutPanel { Dock = DockStyle.Fill, ColumnCount = 1, RowCount = 2, Padding = new Padding(0, 12, 0, 0) }; panel.RowStyles.Add(new RowStyle(SizeType.AutoSize)); panel.RowStyles.Add(new RowStyle(SizeType.Percent, 100)); var commands = new FlowLayoutPanel { AutoSize = true, Dock = DockStyle.Top, FlowDirection = FlowDirection.LeftToRight }; var refresh = new Button { Text = "Aggiorna registro", AutoSize = true }; refresh.Click += (_, _) => RefreshLicenses(); var revoke = new Button { Text = "Segna revocata", AutoSize = true }; revoke.Click += (_, _) => RevokeSelected(); commands.Controls.Add(refresh); commands.Controls.Add(revoke); panel.Controls.Add(commands, 0, 0); _grid.Dock = DockStyle.Fill; _grid.ReadOnly = true; _grid.AllowUserToAddRows = false; _grid.AllowUserToDeleteRows = false; _grid.AutoGenerateColumns = true; _grid.AutoSizeColumnsMode = DataGridViewAutoSizeColumnsMode.Fill; _grid.SelectionMode = DataGridViewSelectionMode.FullRowSelect; _grid.MultiSelect = false; _grid.SelectionChanged += (_, _) => ShowSelectedCode(); panel.Controls.Add(_grid, 0, 1); return panel; } private void IssueLicense() { try { var expiry = new DateTimeOffset(_expiresAt.Value); var license = _repository.Issue(_firstName.Text, _lastName.Text, _email.Text, expiry, _notes.Text); _licenseCode.Text = license.Token; Clipboard.SetText(license.Token); _status.Text = $"Licenza {ShortId(license.LicenseId)} emessa e copiata negli appunti. Scadenza: {license.ExpiresAtUtc.ToLocalTime():dd/MM/yyyy HH:mm}."; RefreshLicenses(license.LicenseId); } catch (Exception error) { MessageBox.Show(this, error.GetBaseException().Message, "Licenza non emessa", MessageBoxButtons.OK, MessageBoxIcon.Warning); } } private void RefreshLicenses(string? selectLicenseId = null) { _licenses = _repository.GetAll(); _grid.DataSource = _licenses.Select(item => new { Id = ShortId(item.LicenseId), item.Name, item.Email, Emessa = item.IssuedAtUtc.ToLocalTime().ToString("dd/MM/yyyy HH:mm"), Scadenza = item.ExpiresAtUtc.ToLocalTime().ToString("dd/MM/yyyy HH:mm"), item.Status, item.Notes, LicenseId = item.LicenseId }).ToList(); if (selectLicenseId is not null) { foreach (DataGridViewRow row in _grid.Rows) { if (Equals(row.Cells["LicenseId"].Value, selectLicenseId)) { row.Selected = true; _grid.CurrentCell = row.Cells[0]; break; } } } if (_grid.Columns.Contains("LicenseId")) { _grid.Columns["LicenseId"]!.Visible = false; } _status.Text = $"{_licenses.Count} licenze registrate. Archivio: {_repository.StoragePath}"; } private void ShowSelectedCode() { var selected = GetSelectedLicense(); if (selected is not null) { _licenseCode.Text = selected.Token; } } private void CopyCurrentCode() { if (!string.IsNullOrWhiteSpace(_licenseCode.Text)) { Clipboard.SetText(_licenseCode.Text); _status.Text = "Codice licenza copiato negli appunti."; } } private void RevokeSelected() { var selected = GetSelectedLicense(); if (selected is null || selected.RevokedAtUtc is not null) { return; } var answer = MessageBox.Show( this, "La revoca verrà annotata nel registro, ma un'app offline continuerà a riconoscere il codice fino alla scadenza. Continuare?", "Segna licenza revocata", MessageBoxButtons.YesNo, MessageBoxIcon.Warning); if (answer != DialogResult.Yes) { return; } _repository.MarkRevoked(selected.LicenseId); RefreshLicenses(selected.LicenseId); } private void ExportSigningKey() { var password = PromptPassword("Password backup", "Scegli una password di almeno 12 caratteri per cifrare la chiave.", true); if (password is null) return; using var dialog = new SaveFileDialog { Title = "Salva backup della chiave licenze", Filter = "Backup chiave LME (*.lmekey)|*.lmekey", FileName = $"LME-Pro-licenze-{DateTime.Now:yyyyMMdd}.lmekey", AddExtension = true }; if (dialog.ShowDialog(this) != DialogResult.OK) return; try { _repository.ExportSigningKeyBackup(dialog.FileName, password); _status.Text = "Backup cifrato della chiave creato. Conserva file e password separatamente."; } catch (Exception error) { MessageBox.Show(this, error.GetBaseException().Message, "Backup non creato", MessageBoxButtons.OK, MessageBoxIcon.Warning); } } private void ImportSigningKey() { using var dialog = new OpenFileDialog { Title = "Apri backup della chiave licenze", Filter = "Backup chiave LME (*.lmekey)|*.lmekey" }; if (dialog.ShowDialog(this) != DialogResult.OK) return; var password = PromptPassword("Password backup", "Inserisci la password usata per cifrare questa chiave.", false); if (password is null) return; var answer = MessageBox.Show( this, "La chiave di emissione corrente verrà sostituita. Importa soltanto il backup associato a questa versione dell'app.", "Ripristina chiave licenze", MessageBoxButtons.YesNo, MessageBoxIcon.Warning); if (answer != DialogResult.Yes) return; try { _repository.ImportSigningKeyBackup(dialog.FileName, password); _status.Text = "Chiave di emissione ripristinata dal backup."; } catch (Exception error) { MessageBox.Show(this, error.GetBaseException().Message, "Chiave non importata", MessageBoxButtons.OK, MessageBoxIcon.Error); } } private string? PromptPassword(string title, string message, bool confirm) { using var dialog = new Form { Text = title, Width = 470, Height = confirm ? 245 : 205, StartPosition = FormStartPosition.CenterParent, FormBorderStyle = FormBorderStyle.FixedDialog, MaximizeBox = false, MinimizeBox = false }; var layout = new TableLayoutPanel { Dock = DockStyle.Fill, Padding = new Padding(14), RowCount = confirm ? 5 : 4, ColumnCount = 1 }; var explanation = new Label { Text = message, AutoSize = true, MaximumSize = new Size(420, 0) }; var password = new TextBox { UseSystemPasswordChar = true, Dock = DockStyle.Top }; var confirmation = new TextBox { UseSystemPasswordChar = true, Dock = DockStyle.Top, Visible = confirm }; var buttons = new FlowLayoutPanel { FlowDirection = FlowDirection.RightToLeft, Dock = DockStyle.Fill, AutoSize = true }; var ok = new Button { Text = "OK", DialogResult = DialogResult.OK, AutoSize = true }; var cancel = new Button { Text = "Annulla", DialogResult = DialogResult.Cancel, AutoSize = true }; buttons.Controls.Add(ok); buttons.Controls.Add(cancel); layout.Controls.Add(explanation); layout.Controls.Add(new Label { Text = "Password", AutoSize = true }); layout.Controls.Add(password); if (confirm) { var confirmationPanel = new TableLayoutPanel { Dock = DockStyle.Top, AutoSize = true, RowCount = 2 }; confirmationPanel.Controls.Add(new Label { Text = "Conferma password", AutoSize = true }); confirmationPanel.Controls.Add(confirmation); layout.Controls.Add(confirmationPanel); } layout.Controls.Add(buttons); dialog.Controls.Add(layout); dialog.AcceptButton = ok; dialog.CancelButton = cancel; if (dialog.ShowDialog(this) != DialogResult.OK) return null; if (confirm && password.Text != confirmation.Text) { MessageBox.Show(this, "Le password non coincidono.", title, MessageBoxButtons.OK, MessageBoxIcon.Warning); return null; } return password.Text; } private IssuedLicense? GetSelectedLicense() { if (_grid.CurrentRow?.Cells["LicenseId"].Value is not string id) { return null; } return _licenses.FirstOrDefault(item => item.LicenseId == id); } private static void AddLabeledControl(TableLayoutPanel form, string labelText, Control control, int column, int row, int columnSpan = 1) { var panel = new TableLayoutPanel { Dock = DockStyle.Fill, AutoSize = true, RowCount = 2, Margin = new Padding(3) }; panel.RowStyles.Add(new RowStyle(SizeType.AutoSize)); panel.RowStyles.Add(new RowStyle(SizeType.AutoSize)); panel.Controls.Add(new Label { Text = labelText, AutoSize = true }, 0, 0); control.Dock = DockStyle.Top; control.Margin = new Padding(0, 4, 0, 0); panel.Controls.Add(control, 0, 1); form.Controls.Add(panel, column, row); if (columnSpan > 1) { form.SetColumnSpan(panel, columnSpan); } } private static string ShortId(string id) => id.Length > 8 ? id[..8].ToUpperInvariant() : id; }