feat: Implement admin-only specialties management interface
Playwright Tests / test (push) Canceled after 0s
Playwright Tests / test (push) Canceled after 0s
- Added MailerAttachmentPolicy for validating email attachments against server rules. - Introduced fis.specialties.js for managing hidden maintenance actions via an admin interface. - Created fis.specialties.scss for styling the specialties management UI. - Added German localization for specialties management in fis.specialties_txt_de.js. - Developed CSS for specialties management in fis.specialties.css. - Implemented server-side stored procedure fds__admin_backupAndClearInvoiceFile for backing up and clearing invoice files. - Created fds__invoice_file_backups table to store backup records of invoice files.
This commit is contained in:
@@ -69,13 +69,34 @@ public partial class IntranetController
|
||||
{
|
||||
if (!HasForm("to", "subject"))
|
||||
return BadRequest400();
|
||||
// attachpdf is the dialog's Ja/Nein select ("true"/"false"); absent → attach (default).
|
||||
bool attachPdf = !bool.TryParse(Form("attachpdf"), out bool parsed) || parsed;
|
||||
var result = await status.SendTestEmailAsync(
|
||||
Form("to"), Form("subject"), Form("body"), HttpContext.RequestAborted);
|
||||
_logger.LogInformation("Admin test email requested by user={User} to={To} sent={Sent}",
|
||||
UserAccountID, result.RequestedRecipient, result.Sent);
|
||||
Form("to"), Form("subject"), Form("body"), attachPdf, HttpContext.RequestAborted);
|
||||
_logger.LogInformation("Admin test email requested by user={User} to={To} attachment={Attachment} sent={Sent}",
|
||||
UserAccountID, result.RequestedRecipient, result.AttachmentName, result.Sent);
|
||||
return AdminJson(new { result });
|
||||
}
|
||||
|
||||
// Maintenance action, reachable only from the admin-gated "Specialties" page
|
||||
// (IntranetController.Specialties.cs / fis.specialties.js): backs up an invoice's
|
||||
// stored PDF and clears it so the document regenerates on next read (ADR 0025 /
|
||||
// Docs/Decisions/0027). GET-safe crawlers/prefetchers must never trigger this, so it
|
||||
// requires POST despite the rest of the Admin module tolerating either verb.
|
||||
case "backupclearinvoicefile":
|
||||
{
|
||||
if (!HttpContext.Request.Method.Equals("POST", StringComparison.OrdinalIgnoreCase))
|
||||
return BadRequest400();
|
||||
string invoiceId = string.IsNullOrWhiteSpace(code) ? Form("id") : code;
|
||||
if (string.IsNullOrWhiteSpace(invoiceId))
|
||||
return BadRequest400();
|
||||
var backup = await _invoiceFileBackup.BackupAndClearAsync(invoiceId, UserAccountID, DbSec, HttpContext.RequestAborted);
|
||||
_logger.LogWarning(
|
||||
"Admin invoice-file backup/clear requested by user={User} invoiceId={InvoiceId} success={Success} reason={Reason}",
|
||||
UserAccountID, invoiceId, backup.Success, backup.Reason);
|
||||
return AdminJson(new { backup });
|
||||
}
|
||||
|
||||
default:
|
||||
_logger.LogWarning("Admin: no handler for action={Action}, user={User}", id, UserAccountID);
|
||||
return BadRequest400();
|
||||
|
||||
@@ -155,8 +155,11 @@ public partial class IntranetController
|
||||
{
|
||||
var remdoc = new Dictionary<string, byte[]>
|
||||
{ [frdic.nz("DocumentName", "").ne("Zahlungserinnerung.pdf")] = filebyte };
|
||||
// The invoice copy is optional: an empty varbinary (invoice file never stored) must
|
||||
// not attach a 0-byte file — the mailer refuses empty attachments and the whole
|
||||
// reminder would fail instead of going out without the copy.
|
||||
if (!string.IsNullOrEmpty(frdic.nz("InvoiceFileName")) &&
|
||||
frdic.no("InvoiceFile", null!) is byte[] invFile)
|
||||
frdic.no("InvoiceFile", null!) is byte[] { Length: > 0 } invFile)
|
||||
remdoc[frdic.nz("InvoiceFileName")] = invFile;
|
||||
|
||||
_logger.LogInformation("Reminder conf: emailing finalized reminder {RemId} to {Email} user={User}",
|
||||
@@ -247,8 +250,11 @@ public partial class IntranetController
|
||||
{
|
||||
var remdoc = new Dictionary<string, byte[]>
|
||||
{ [frdic.nz("DocumentName", "").ne("Zahlungserinnerung.pdf")] = filebyte };
|
||||
// The invoice copy is optional: an empty varbinary (invoice file never stored) must
|
||||
// not attach a 0-byte file — the mailer refuses empty attachments and the whole
|
||||
// reminder would fail instead of going out without the copy.
|
||||
if (!string.IsNullOrEmpty(frdic.nz("InvoiceFileName")) &&
|
||||
frdic.no("InvoiceFile", null!) is byte[] invFile)
|
||||
frdic.no("InvoiceFile", null!) is byte[] { Length: > 0 } invFile)
|
||||
remdoc[frdic.nz("InvoiceFileName")] = invFile;
|
||||
bool sent = await _comService.SendEmailAsync($"rem_{remId}",
|
||||
$"SanitärFuchs - {frdic.nz("subject").ne(frdic.nz("DocumentName"))}",
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using static OCORE.web.mvc_helper_async;
|
||||
|
||||
namespace Fuchs.Controllers;
|
||||
|
||||
// Partial class: "Specialties" module — the admin page (URL /specialties, menu entry under
|
||||
// Administration) that surfaces the otherwise hidden maintenance actions as small card forms.
|
||||
//
|
||||
// This partial only answers the module's auth probe; the cards call the real action endpoints
|
||||
// directly (e.g. POST /do/admin/backupclearinvoicefile/<id> in IntranetController.Admin.cs),
|
||||
// each of which enforces the same fds_sys > 4 gate server-side. See
|
||||
// Docs/Concepts/admin-specialties.md.
|
||||
public partial class IntranetController
|
||||
{
|
||||
private async Task<IActionResult> Do_Process_Specialties(string fn, string id, string code)
|
||||
{
|
||||
_logger.LogDebug("Do_Process_Specialties action={Action} user={User}", id, UserAccountID);
|
||||
|
||||
int authLevel = await GetSystemAdminAuthAsync(fn, id, code);
|
||||
bool authorized = authLevel > AdminMinAuthExclusive;
|
||||
|
||||
if (id.Equals("auth", StringComparison.OrdinalIgnoreCase))
|
||||
return await JSONAsync(new { manage = authorized ? 1 : 0, level = authLevel });
|
||||
|
||||
if (!authorized)
|
||||
{
|
||||
_logger.LogWarning("Specialties access denied for user={User} (fds_sys={Level}) action={Action}",
|
||||
UserAccountID, authLevel, id);
|
||||
return Unauthorized401();
|
||||
}
|
||||
|
||||
_logger.LogWarning("Specialties: no handler for action={Action}, user={User}", id, UserAccountID);
|
||||
return BadRequest400();
|
||||
}
|
||||
}
|
||||
@@ -33,6 +33,7 @@ public partial class IntranetController : Microsoft.AspNetCore.Mvc.Controller
|
||||
private readonly IWidgetService _widgets;
|
||||
private readonly IReportService _reports;
|
||||
private readonly IInvoiceService _invoices;
|
||||
private readonly IInvoiceFileBackupService _invoiceFileBackup;
|
||||
private readonly IReminderService _reminders;
|
||||
private readonly IEventService _events;
|
||||
private readonly IInvoiceDraftService _invoiceDrafts;
|
||||
@@ -65,6 +66,7 @@ public partial class IntranetController : Microsoft.AspNetCore.Mvc.Controller
|
||||
IWidgetService widgets,
|
||||
IReportService reports,
|
||||
IInvoiceService invoices,
|
||||
IInvoiceFileBackupService invoiceFileBackup,
|
||||
IReminderService reminders,
|
||||
IEventService events,
|
||||
IInvoiceDraftService invoiceDrafts,
|
||||
@@ -82,6 +84,7 @@ public partial class IntranetController : Microsoft.AspNetCore.Mvc.Controller
|
||||
_widgets = widgets;
|
||||
_reports = reports;
|
||||
_invoices = invoices;
|
||||
_invoiceFileBackup = invoiceFileBackup;
|
||||
_reminders = reminders;
|
||||
_events = events;
|
||||
_invoiceDrafts = invoiceDrafts;
|
||||
@@ -185,6 +188,7 @@ public partial class IntranetController : Microsoft.AspNetCore.Mvc.Controller
|
||||
"rep" => await Do_Process_Reports(fn, id, code),
|
||||
"bam" => await Do_Process_Bankings(fn, id, code),
|
||||
"admin" => await Do_Process_Admin(fn, id, code),
|
||||
"specialties" => await Do_Process_Specialties(fn, id, code),
|
||||
"usr" => await Do_Process_AdminUsers(fn, id, code),
|
||||
"usrgrp" => await Do_Process_AdminGroups(fn, id, code),
|
||||
"auth" => await HandleAuth(fn, id, code),
|
||||
|
||||
Reference in New Issue
Block a user