fix: improve client information form and password reveal

This commit is contained in:
Marco0300
2026-09-01 22:30:57 +02:00
parent 0b1cb20578
commit 6d0d94a599
+23 -4
View File
@@ -138,6 +138,25 @@ if ($route === 'attachment') {
readfile($path); exit;
}
if ($route === 'credential_reveal') {
require_permission('credentials.view');
if (($_SERVER['REQUEST_METHOD'] ?? 'GET') !== 'POST') { http_response_code(405); exit('Reveal requires POST'); }
verify_csrf();
$credentialId = filter_var(scalar_input($_POST['credential_id'] ?? null), FILTER_VALIDATE_INT);
$credentialStmt = db()->prepare('SELECT id, client_id, secret_ciphertext FROM credentials WHERE id = :id AND is_active = 1');
$credentialStmt->execute(['id' => $credentialId]);
$credential = $credentialStmt->fetch();
if (!$credential || !can_access_client((int)$credential['client_id'])) { http_response_code(404); header('Content-Type: application/json'); echo json_encode(['ok' => false, 'error' => 'Credential not found.']); exit; }
try {
$secret = (new \App\Domain\Credential\CredentialVault())->decrypt($credential['secret_ciphertext']);
audit('credential_revealed', 'credential', (int)$credential['id'], ['client_id' => (int)$credential['client_id']]);
header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0');
header('Content-Type: application/json; charset=UTF-8');
echo json_encode(['ok' => true, 'secret' => $secret], JSON_THROW_ON_ERROR);
} catch (Throwable) { http_response_code(500); header('Content-Type: application/json'); echo json_encode(['ok' => false, 'error' => 'Credential could not be decrypted.']); }
exit;
}
if ($route === 'client_history') {
require_permission('clients.view');
$clientId = filter_var(scalar_input($_GET['id'] ?? null), FILTER_VALIDATE_INT);
@@ -529,7 +548,7 @@ if ($route === 'client') {
$credentialRows = $credentialStmt->fetchAll();
}
render_header('Client details');
echo '<div class="client-tabs" role="tablist"><button class="client-tab active" data-client-tab="overview">Overview</button><button class="client-tab" data-client-tab="contacts">Contacts</button><button class="client-tab" data-client-tab="sla">SLA</button><button class="client-tab" data-client-tab="technical">Client information</button><a class="client-tab" href="/?route=client_history&id=' . (int)$clientId . '">Jobcard history</a></div><script>document.addEventListener("DOMContentLoaded",function(){const tabs=[...document.querySelectorAll("[data-client-tab]")];const sections=[...document.querySelectorAll("[data-client-section]")];function activate(key){tabs.forEach(t=>t.classList.toggle("active",t.dataset.clientTab===key));sections.forEach(s=>s.classList.toggle("client-section-hidden",s.dataset.clientSection!==key));}tabs.forEach(t=>t.addEventListener("click",()=>activate(t.dataset.clientTab)));activate("overview");});</script>';
echo '<div class="client-tabs" role="tablist"><button class="client-tab active" data-client-tab="overview">Overview</button><button class="client-tab" data-client-tab="contacts">Contacts</button><button class="client-tab" data-client-tab="sla">SLA</button><button class="client-tab" data-client-tab="technical">Client information</button><a class="client-tab" href="/?route=client_history&id=' . (int)$clientId . '">Jobcard history</a></div><script>document.addEventListener("DOMContentLoaded",function(){const tabs=[...document.querySelectorAll("[data-client-tab]")];const sections=[...document.querySelectorAll("[data-client-section]")];function activate(key){tabs.forEach(t=>t.classList.toggle("active",t.dataset.clientTab===key));sections.forEach(s=>s.classList.toggle("client-section-hidden",s.dataset.clientSection!==key));}tabs.forEach(t=>t.addEventListener("click",()=>activate(t.dataset.clientTab)));document.querySelectorAll(".js-reveal-credential").forEach(button=>button.addEventListener("click",async()=>{const id=button.dataset.credentialId;const target=document.querySelector(".credential-secret[data-credential-id=\""+id+"\"]");if(button.dataset.revealed==="1"){target.textContent="••••••••••••••••••••";button.textContent="Reveal password";button.dataset.revealed="0";return;}button.disabled=true;try{const body=new URLSearchParams({_csrf:document.querySelector("input[name=_csrf]").value,credential_id:id});const response=await fetch("/?route=credential_reveal",{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded","Accept":"application/json"},body});const result=await response.json();if(!response.ok||!result.ok)throw new Error(result.error||"Unable to reveal password");target.textContent=result.secret;button.textContent="Hide password";button.dataset.revealed="1";}catch(error){window.alert(error.message);}finally{button.disabled=false;}}));activate("overview");});</script>';
echo '<div class="d-flex justify-content-between align-items-center mb-4"><div><a href="/?route=clients" class="text-decoration-none">← Back to clients</a> <a href="/?route=client_history&id=' . (int)$clientId . '" class="text-decoration-none">View history</a><h1 class="h3 mt-2 mb-1">' . e($client['name']) . '</h1><p class="text-muted mb-0">Client profile and support contacts.</p></div><span class="badge text-bg-' . ($client['status'] === 'active' ? 'success' : 'secondary') . '">' . e(ucfirst($client['status'])) . '</span></div>' . (isset($_GET['contact_created']) ? '<div class="alert alert-success">Contact added successfully.</div>' : '') . (isset($_GET['sla_updated']) ? '<div class="alert alert-success">SLA agreement updated.</div>' : '') . ($contactErrors ? '<div class="alert alert-danger">' . e(implode(' ', $contactErrors)) . '</div>' : '') . ($slaErrors ? '<div class="alert alert-danger">' . e(implode(' ', $slaErrors)) . '</div>' : '') . '<div class="row g-4"><div class="col-lg-6"><div class="card h-100" data-client-section="overview"><div class="card-body"><h2 class="h5">Support information</h2><dl class="row mb-0"><dt class="col-sm-5">Email</dt><dd class="col-sm-7">' . e((string)($client['support_email'] ?? '—')) . '</dd><dt class="col-sm-5">Phone</dt><dd class="col-sm-7">' . e((string)($client['support_phone'] ?? '—')) . '</dd><dt class="col-sm-5">Preferred method</dt><dd class="col-sm-7">' . e((string)($client['preferred_contact_method'] ?? '—')) . '</dd><dt class="col-sm-5">Address</dt><dd class="col-sm-7">' . nl2br(e((string)($client['physical_address'] ?? '—'))) . '</dd></dl></div></div></div><div class="col-lg-6"><div class="card h-100" data-client-section="contacts"><div class="card-body"><h2 class="h5">Contacts</h2>';
if (!$contacts) echo '<p class="text-muted mb-0">No contacts recorded.</p>';
foreach ($contacts as $contact) { echo '<div class="border-bottom py-2"><div class="fw-semibold">' . e($contact['name']) . ($contact['is_primary'] ? ' <span class="badge text-bg-primary">Primary</span>' : '') . '</div><div class="small text-muted">' . e((string)($contact['email'] ?? '')) . ' ' . e((string)($contact['phone'] ?? '')) . '</div>'; if (can('clients.manage')) { echo '<form method="post" class="d-inline me-2"><input type="hidden" name="_csrf" value="' . e(csrf_token()) . '"><input type="hidden" name="action" value="contact_primary"><input type="hidden" name="contact_id" value="' . (int)$contact['id'] . '"><button class="btn btn-sm btn-link p-0">Set primary</button></form><form method="post" class="d-inline"><input type="hidden" name="_csrf" value="' . e(csrf_token()) . '"><input type="hidden" name="action" value="contact_delete"><input type="hidden" name="contact_id" value="' . (int)$contact['id'] . '"><button class="btn btn-sm btn-link text-danger p-0">Delete</button></form><form method="post" class="row g-1 mt-1"><input type="hidden" name="_csrf" value="' . e(csrf_token()) . '"><input type="hidden" name="action" value="contact_edit"><input type="hidden" name="contact_id" value="' . (int)$contact['id'] . '"><div class="col-md-3"><input class="form-control form-control-sm" name="name" value="' . e($contact['name']) . '" required></div><div class="col-md-3"><input class="form-control form-control-sm" type="email" name="email" value="' . e((string)($contact['email'] ?? '')) . '"></div><div class="col-md-3"><input class="form-control form-control-sm" name="phone" value="' . e((string)($contact['phone'] ?? '')) . '"></div><div class="col-md-3"><button class="btn btn-sm btn-outline-secondary">Save edit</button></div></form>'; } echo '</div>'; }
@@ -547,8 +566,8 @@ if ($route === 'client') {
if ($credentialErrors) echo '<div class="alert alert-danger">' . e(implode(' ', $credentialErrors)) . '</div>';
if (isset($_GET['credential_created'])) echo '<div class="alert alert-success">Credential saved securely.</div>';
foreach ($credentialRows as $credentialRow) {
echo '<div class="border-bottom py-2"><strong>' . e($credentialRow['label']) . '</strong> <span class="badge text-bg-secondary">' . e($credentialRow['category']) . '</span><div class="small text-muted">Username: ' . e((string)($credentialRow['username'] ?? '—')) . ' · Secret: ' . ($revealedCredential && $revealedCredential['id'] === (int)$credentialRow['id'] ? '<code>' . e($revealedCredential['secret']) . '</code>' : '••••••••••••••••••••') . '</div>';
if (can('credentials.view') && !($revealedCredential && $revealedCredential['id'] === (int)$credentialRow['id'])) echo '<form method="post" class="d-inline"><input type="hidden" name="_csrf" value="' . e(csrf_token()) . '"><input type="hidden" name="action" value="credential_reveal"><input type="hidden" name="credential_id" value="' . (int)$credentialRow['id'] . '"><button class="btn btn-sm btn-link p-0">Reveal once</button></form>';
echo '<div class="border-bottom py-2"><strong>' . e($credentialRow['label']) . '</strong><div class="small text-muted">Username: ' . e((string)($credentialRow['username'] ?? '—')) . ' · Password: <span class="credential-secret" data-credential-id="' . (int)$credentialRow['id'] . '">••••••••••••••••••••</span></div>';
if (can('credentials.view')) echo ' <button type="button" class="btn btn-sm btn-link p-0 js-reveal-credential" data-credential-id="' . (int)$credentialRow['id'] . '">Reveal password</button>';
echo '</div>';
}
echo '</div></div>';
@@ -557,7 +576,7 @@ if ($route === 'client') {
$technicalRows = (new \App\Domain\Credential\TechnicalInformationRepository(db()))->forClient($clientId);
echo '<div class="card mt-4" data-client-section="technical"><div class="card-body"><h2 class="h5">Technical information</h2>';
foreach ($technicalRows as $technical) { $display = $technical['display']; echo '<div class="border-bottom py-2"><strong>' . e(ucfirst($technical['category'])) . '</strong><div>' . e((string)$display['label']) . ($display['username'] ? ' · ' . e((string)$display['username']) : '') . '</div><div class="small text-muted">' . nl2br(e((string)($display['notes'] ?? ''))) . '</div></div>'; }
if (can('credentials.manage')) { echo '<hr><h3 class="h6">Add client information</h3><form method="post" class="row g-3"><input type="hidden" name="_csrf" value="' . e(csrf_token()) . '"><input type="hidden" name="action" value="credential"><input type="hidden" name="category" value="other"><div class="col-md-6"><label class="form-label">Name</label><input class="form-control" name="label" placeholder="e.g. Main server" required></div><div class="col-md-6"><label class="form-label">Username</label><input class="form-control" name="username" placeholder="Username"></div><div class="col-md-6"><label class="form-label">Password</label><input class="form-control" type="password" name="secret" placeholder="Password" required></div><div class="col-md-6"><label class="form-label">Extra info</label><input class="form-control" name="credential_notes" placeholder="Extra information"></div><div class="col-12"><button class="btn btn-primary">Save client information securely</button></div></form>'; }
if (can('credentials.manage')) { echo '<hr><h3 class="h6">Add client information</h3><form method="post" class="row g-3"><input type="hidden" name="_csrf" value="' . e(csrf_token()) . '"><input type="hidden" name="action" value="credential"><input type="hidden" name="category" value="other"><div class="col-md-6"><label class="form-label">Name</label><input class="form-control" name="label" placeholder="e.g. Main server" required></div><div class="col-md-6"><label class="form-label">Username</label><input class="form-control" name="username" placeholder="Username"></div><div class="col-md-6"><label class="form-label">Password</label><input class="form-control" type="password" name="secret" placeholder="Password" required></div><div class="col-md-6"><label class="form-label">Extra info</label><textarea class="form-control" name="credential_notes" rows="5" placeholder="Extra information"></textarea></div><div class="col-12"><button class="btn btn-primary">Save client information securely</button></div></form>'; }
echo '</div></div>';
}
if (can('clients.manage')) echo '<div class="card mt-4" data-client-section="overview"><div class="card-body"><h2 class="h5">Edit client</h2><form method="post" class="row g-3"><input type="hidden" name="_csrf" value="' . e(csrf_token()) . '"><input type="hidden" name="action" value="client_update"><div class="col-md-6"><label class="form-label">Client name</label><input class="form-control" name="name" value="' . e($client['name']) . '" required></div><div class="col-md-3"><label class="form-label">Status</label><select class="form-select" name="status"><option value="active"' . ($client['status'] === 'active' ? ' selected' : '') . '>Active</option><option value="inactive"' . ($client['status'] === 'inactive' ? ' selected' : '') . '>Inactive</option></select></div><div class="col-md-3"><label class="form-label">Preferred contact</label><input class="form-control" name="preferred_contact_method" value="' . e((string)($client['preferred_contact_method'] ?? '')) . '"></div><div class="col-md-6"><label class="form-label">Support email</label><input class="form-control" type="email" name="support_email" value="' . e((string)($client['support_email'] ?? '')) . '"></div><div class="col-md-6"><label class="form-label">Support phone</label><input class="form-control" name="support_phone" value="' . e((string)($client['support_phone'] ?? '')) . '"></div><div class="col-md-6"><label class="form-label">Physical address</label><textarea class="form-control" name="physical_address" rows="3">' . e((string)($client['physical_address'] ?? '')) . '</textarea></div><div class="col-md-6"><label class="form-label">Postal address</label><textarea class="form-control" name="postal_address" rows="3">' . e((string)($client['postal_address'] ?? '')) . '</textarea></div><div class="col-12"><label class="form-label">General notes</label><textarea class="form-control" name="general_notes" rows="3">' . e((string)($client['general_notes'] ?? '')) . '</textarea></div><div class="col-12"><button class="btn btn-primary">Save client</button></div></form></div></div>';