Merge pull request 'Feature: APP_KEY Status & Generator in den Einstellungen' (#82) from fds2610-bot/Steuerkram:main into main

Reviewed-on: https://codeberg.org/fds2610/Steuerkram/pulls/82
This commit is contained in:
fds2610 2026-05-22 11:49:10 +02:00
commit 1efce29956
4 changed files with 123 additions and 0 deletions

View file

@ -2,6 +2,14 @@
--- ---
## 2026-05-22 – Feature: APP_KEY Status & Generator in den Einstellungen
- Neuer Settings-Bereich unter dem Betriebsmodus zeigt Status des Verschlüsselungsschlüssels (`APP_KEY`): **Gesetzt** (grün, 64 Hex-Zeichen valide), **Ungültig** (falsches Format) oder **Fehlt** (leer)
- Bei fehlendem oder ungültigem Schlüssel: Button „Neuen Schlüssel generieren" → POST `/settings/generate-app-key` liefert `bin2hex(random_bytes(32))`, Anzeige im Code-Block mit Copy-Button
- Schlüssel wird nicht persistiert (Container hat keinen Schreibzugriff auf `.env`); Anleitung im UI: kopieren → in `.env` eintragen → `docker compose restart web`
---
## 2026-05-22 – UI: Einheitliche Icon-Buttons für Bearbeiten/Ansehen/Löschen ## 2026-05-22 – UI: Einheitliche Icon-Buttons für Bearbeiten/Ansehen/Löschen
- **Bleistift-Icon** (transparent, ohne Hintergrund – Stil der Direktverbindungs-Kacheln) ersetzt alle „Bearbeiten"- und „Öffnen"-Buttons in `accounts/list`, `customers/show`, `customers/list`, `suppliers/list`, `useradmin/list`, `assets/list`, `belege/list`, `invoices/list`, `invoices/show` - **Bleistift-Icon** (transparent, ohne Hintergrund – Stil der Direktverbindungs-Kacheln) ersetzt alle „Bearbeiten"- und „Öffnen"-Buttons in `accounts/list`, `customers/show`, `customers/list`, `suppliers/list`, `useradmin/list`, `assets/list`, `belege/list`, `invoices/list`, `invoices/show`

View file

@ -159,6 +159,7 @@ $router->post('/settings/mail-template', fn($p) => (new SettingsController())->m
$router->post('/settings/mail-test', fn($p) => (new SettingsController())->mailTest($p)); $router->post('/settings/mail-test', fn($p) => (new SettingsController())->mailTest($p));
$router->post('/settings/logo', fn($p) => (new SettingsController())->uploadLogo($p)); $router->post('/settings/logo', fn($p) => (new SettingsController())->uploadLogo($p));
$router->post('/settings/logo/delete', fn($p) => (new SettingsController())->deleteLogo($p)); $router->post('/settings/logo/delete', fn($p) => (new SettingsController())->deleteLogo($p));
$router->post('/settings/generate-app-key', fn($p) => (new SettingsController())->generateAppKey($p));
$router->get('/settings/bankimport', fn($p) => (new SettingsController())->bankImport($p)); $router->get('/settings/bankimport', fn($p) => (new SettingsController())->bankImport($p));
$router->post('/settings/bankimport/suggest', fn($p) => (new SettingsController())->storeSuggestion($p)); $router->post('/settings/bankimport/suggest', fn($p) => (new SettingsController())->storeSuggestion($p));
$router->post('/settings/bankimport/create', fn($p) => (new SettingsController())->createBankImportFormat($p)); $router->post('/settings/bankimport/create', fn($p) => (new SettingsController())->createBankImportFormat($p));

View file

@ -139,6 +139,12 @@ class SettingsController extends BaseController
View::redirect('/settings', 'Logo gelöscht.'); View::redirect('/settings', 'Logo gelöscht.');
} }
public function generateAppKey(array $params): void
{
header('Content-Type: application/json');
echo json_encode(['key' => bin2hex(random_bytes(32))]);
}
public function bankImport(array $params): void public function bankImport(array $params): void
{ {
$formats = BankSource::all('csv'); $formats = BankSource::all('csv');

View file

@ -176,6 +176,71 @@
</div> </div>
</div> </div>
<!-- Verschlüsselungs-Schlüssel (APP_KEY) -->
<?php
$appKey = (string)\Steuerkram\Config::get('app_key', '');
$appKeyValid = $appKey !== '' && preg_match('/^[0-9a-f]{64}$/i', $appKey) === 1;
$appKeyColor = $appKeyValid ? 'var(--color-success,#22c55e)' : 'var(--color-danger,#dc2626)';
?>
<div class="card mb-2" style="border:2px solid <?= $appKeyColor ?>">
<div class="card-header">
<div class="card-title">App-Verschlüsselungsschlüssel</div>
<?php if ($appKeyValid): ?>
<span class="badge badge-green">Gesetzt</span>
<?php elseif ($appKey !== ''): ?>
<span class="badge badge-red">Ungültig</span>
<?php else: ?>
<span class="badge badge-red">Fehlt</span>
<?php endif; ?>
</div>
<div class="card-body">
<p class="text-sm" style="margin:0 0 .75rem 0">
<?php if ($appKeyValid): ?>
<span style="color:#166534">✓ <code>APP_KEY</code> ist gültig (64-stellige Hex-Zeichenkette).</span><br>
Wird zum Verschlüsseln sensibler Felder (PayPal-Signature, FinTS-PIN) genutzt.
<strong>Niemals ändern</strong> – sonst sind alle bestehenden verschlüsselten Werte verloren.
<?php elseif ($appKey !== ''): ?>
<span style="color:#dc2626">✗ <code>APP_KEY</code> ist gesetzt, aber kein gültiges Format (erwartet: 64 Hex-Zeichen).</span><br>
Bitte prüfen und korrigieren.
<?php else: ?>
<span style="color:#dc2626">✗ <code>APP_KEY</code> ist nicht gesetzt.</span><br>
Ohne Schlüssel können keine Bank-Direktverbindungen mit PIN/Secret gespeichert werden.
<?php endif; ?>
</p>
<?php if (!$appKeyValid): ?>
<button type="button" id="btn-generate-app-key" class="btn btn-primary btn-sm">
Neuen Schlüssel generieren
</button>
<?php endif; ?>
<div id="app-key-result" style="display:none;margin-top:.75rem">
<div class="text-sm" style="margin-bottom:.4rem;font-weight:600">Neuer Schlüssel:</div>
<div style="display:flex;gap:.5rem;align-items:stretch">
<code id="app-key-value"
style="flex:1;padding:.5rem .6rem;background:#f1f5f9;border:1px solid var(--color-border,#e2e8f0);border-radius:var(--radius);font-family:monospace;font-size:.8rem;word-break:break-all;user-select:all"></code>
<button type="button" id="btn-copy-app-key" class="btn btn-secondary btn-sm" title="In Zwischenablage kopieren">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24"
fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<rect x="9" y="9" width="13" height="13" rx="2"/>
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/>
</svg>
</button>
</div>
<div class="text-sm" style="margin-top:.75rem;color:#92400e;background:#fffbeb;border:1px solid #fde68a;border-radius:var(--radius);padding:.6rem .75rem">
<strong>So aktivierst du den Schlüssel:</strong>
<ol style="margin:.4rem 0 0 1.2rem;padding:0">
<li>Schlüssel oben kopieren.</li>
<li>Auf dem Host in <code>.env</code> die Zeile <code>APP_KEY=...</code> mit dem neuen Wert ergänzen.</li>
<li>Container neu starten: <code>docker compose restart web</code></li>
</ol>
Der Schlüssel wird <strong>nur einmal angezeigt</strong> und nirgends gespeichert.
Sichere ihn auch im Backup mit – ohne ihn sind alle verschlüsselten Werte verloren.
</div>
</div>
</div>
</div>
<!-- KI-Kontierungsvorschlag --> <!-- KI-Kontierungsvorschlag -->
<div class="card mb-2"> <div class="card mb-2">
<div class="card-header"><div class="card-title">KI-Kontierungsvorschlag</div></div> <div class="card-header"><div class="card-title">KI-Kontierungsvorschlag</div></div>
@ -544,4 +609,47 @@ document.getElementById('btn-mail-text-save')?.addEventListener('click', functio
}) })
.finally(function () { btn.disabled = false; }); .finally(function () { btn.disabled = false; });
}); });
document.getElementById('btn-generate-app-key')?.addEventListener('click', function () {
const btn = this;
const result = document.getElementById('app-key-result');
const out = document.getElementById('app-key-value');
btn.disabled = true;
fetch('/settings/generate-app-key', { method: 'POST' })
.then(r => r.json())
.then(data => {
if (data && data.key) {
out.textContent = data.key;
result.style.display = 'block';
btn.style.display = 'none';
} else {
alert('Schlüssel konnte nicht erzeugt werden.');
btn.disabled = false;
}
})
.catch(() => {
alert('Netzwerkfehler beim Erzeugen des Schlüssels.');
btn.disabled = false;
});
});
document.getElementById('btn-copy-app-key')?.addEventListener('click', function () {
const out = document.getElementById('app-key-value');
const txt = out.textContent || '';
if (!txt) return;
const btn = this;
const restore = btn.innerHTML;
navigator.clipboard.writeText(txt)
.then(() => {
btn.textContent = '✓';
setTimeout(() => { btn.innerHTML = restore; }, 1500);
})
.catch(() => {
const range = document.createRange();
range.selectNodeContents(out);
const sel = window.getSelection();
sel.removeAllRanges();
sel.addRange(range);
});
});
</script> </script>