Files
itflow/agent/modals/software/software.php
johnnyq fecd1cabff Credentials makeover: rebuilt credentials list with kind icons, username copy, password show/copy and live 2FA code buttons fetched on demand, where it opens, what it is for (asset, contact, vendors, licenses, services), password age pills and last used, filters for Starred, With 2FA, No 2FA, Not changed in 90 days and Shared with counts, search across notes, second link, tags, asset and contact, open straight to a credential with ?open=ID; new credential quick look with login, age, last viewed by, links with unlink, notes, open share links with turn off, and history; tabbed new/edit form with password generator (passphrase or random), strength meter, 2FA secret that accepts spaces or an otpauth:// link and shows the current code to check, copy buttons on both links, prefill from asset, contact or vendor; new Change password window that dates and logs the change; new Share window from anywhere (contact or email, 1 hour to 1 month, gone after opened once by default, link copied); same login controls on asset and contact pages and quick looks, vendor Logins tab, license page and quick look, service quick look, client overview favourites and search; credential pickers show icons and say when a credential moves from another asset or contact; client overview Passwords due for a change attention item and Active share links limited to live links; rotation report with selectable span, per client counts and oldest first; more export columns; demo data gets 2FA secrets, varied password ages and logins attached to their assets. Fixes passwords being blanked when the vault session had expired, other clients' assets, contacts and tags accepted on save, bulk star using the wrong permission, credential links to assets and contacts not checking the client, the edit or link permission, or which asset or contact an unlink was for, credentials left pointing at deleted assets and contacts, share links left open after a credential was deleted, import carrying values between rows and accepting any file type, 2FA secrets saved with spaces or lowercase giving no code, the service quick look listing credentials without credential access and repeating assets, an asset login with only a username being lost, and the API not dating new passwords. Adds functions/credential.php and js/credentials.js; removes credential_model.php and the old credential show password/OTP scripts. No DB change.
2026-10-14 23:33:29 -04:00

296 lines
17 KiB
PHP

<?php
/*
* License quick look - the license page in a modal, opened from the asset
* and contact pages, the license list, a document's or file's links and the
* search. Header with renewal and seat state, the facts to copy (key, order
* reference, vendor account and phone), quick notes, then tabs for only what
* has something in it: devices, users, keys, credentials, documents, files.
*/
require_once '../../../includes/modal_header.php';
enforceUserPermission('module_support');
$software_id = intval($_GET['id'] ?? 0);
$license = softwareRow($software_id);
if (!$license) {
echo json_encode(['error' => 'That license no longer exists.']);
exit;
}
$client_id = intval($license['software_client_id']);
enforceClientAccess();
$software_name = escapeHtml($license['software_name']);
$software_version = escapeHtml($license['software_version']);
$software_description = escapeHtml($license['software_description']);
$software_purchase_reference = escapeHtml($license['software_purchase_reference']);
$software_notes = escapeHtml($license['software_notes']);
$software_archived = !empty($license['software_archived_at']);
$vendor_id = intval($license['vendor_id']);
$vendor_name = escapeHtml($license['vendor_name']);
$vendor_account = escapeHtml($license['vendor_account_number']);
$vendor_phone = contactPhoneLink($license['vendor_phone'], $license['vendor_phone_country_code'], $license['vendor_extension']);
$seats = softwareSeats($license['seats_used'], $license['software_seats']);
$renewal = softwareRenewal($license['software_expire']);
$can_edit = lookupUserPermission('module_support') >= 2 && !$software_archived;
$can_credentials = lookupUserPermission('module_credential') >= 1;
$holders = softwareSeatHolders($software_id);
$keys = softwareKeys($software_id);
$credentials = $can_credentials ? credentialRows("credential_id IN (SELECT credential_id FROM software_credentials WHERE software_id = $software_id)") : [];
$documents = linkedRecords('software', $software_id, 'document');
$files = linkedRecords('software', $software_id, 'file');
$quick_tabs = array_filter([
'devices' => ['label' => 'Devices', 'count' => count($holders['assets'])],
'users' => ['label' => 'Users', 'count' => count($holders['contacts'])],
'keys' => ['label' => 'Keys', 'count' => count($keys)],
'credentials' => ['label' => 'Credentials', 'count' => count($credentials)],
'documents' => ['label' => 'Documents', 'count' => count($documents)],
'files' => ['label' => 'Files', 'count' => count($files)],
], function ($tab) {
return $tab['count'] > 0;
});
$first_tab = array_key_first($quick_tabs);
$quick_id = "license-quick-$software_id";
ob_start();
?>
<div class="modal-header bg-dark" data-bs-theme="dark">
<div class="d-flex align-items-center gap-3 min-w-0">
<?= softwareTile($license['software_type']) ?>
<div class="min-w-0">
<h5 class="modal-title d-flex flex-wrap align-items-center column-gap-2 row-gap-1">
<span class="fw-semibold text-break"><?= $software_name ?></span>
<?php if ($software_version) { ?><span class="text-body-secondary fw-normal fs-6"><?= $software_version ?></span><?php } ?>
<?php if (intval($license['software_favorite'])) { ?><i class="fas fa-star text-warning fs-6" title="Favorite"></i><?php } ?>
<?php if ($software_archived) { ?><span class="badge rounded-pill text-bg-secondary fw-normal fs-6">Archived</span><?php } ?>
<span class="fs-6"><?= $renewal && $renewal['state'] !== 'active' ? softwareRenewalPill($renewal) : '' ?></span>
<?php if ($seats['state'] === 'over') { ?>
<span class="badge rounded-pill bg-danger-subtle text-danger-emphasis border border-danger-subtle fw-normal fs-6"><?= $seats['over'] ?> over seats</span>
<?php } ?>
</h5>
<div class="small text-body-secondary d-flex flex-wrap column-gap-3 mt-1">
<?php if ($license['software_type']) { ?><span><?= escapeHtml($license['software_type']) ?></span><?php } ?>
<?php if ($license['software_license_type']) { ?><span><i class="fas fa-fw <?= softwareLicenseTypeIcon($license['software_license_type']) ?> me-1"></i><?= escapeHtml($license['software_license_type']) ?></span><?php } ?>
<span><?= escapeHtml($license['client_name']) ?></span>
</div>
</div>
</div>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<?php if ($software_description) { ?>
<div class="text-body-secondary mb-3"><?= $software_description ?></div>
<?php } ?>
<!-- At a glance - the icons say what each line is -->
<div class="row g-2 itflow-asset-glance">
<div class="col-sm-6">
<div class="itflow-glance-item" title="Seats">
<i class="fas fa-fw fa-users"></i>
<div class="flex-grow-1 min-w-0 itflow-seats-glance"><?= softwareSeatBar($seats) ?></div>
</div>
<?php if ($license['software_key']) { ?>
<div class="itflow-glance-item" title="License key">
<i class="fas fa-fw fa-key"></i>
<span class="min-w-0"><?= softwareKeyHtml($license['software_key']) ?></span>
</div>
<?php } elseif ($keys) { ?>
<div class="itflow-glance-item" title="Keys">
<i class="fas fa-fw fa-key"></i>
<span><?= count($keys) ?> individual key<?= count($keys) === 1 ? '' : 's' ?></span>
</div>
<?php } ?>
<?php if ($software_purchase_reference) { ?>
<div class="itflow-glance-item" title="Order reference">
<i class="fas fa-fw fa-receipt"></i>
<span><?= $software_purchase_reference ?><button type="button" class="itflow-copy clipboardjs" title="Copy reference" data-clipboard-text="<?= $software_purchase_reference ?>"><i class="far fa-copy"></i></button></span>
</div>
<?php } ?>
</div>
<div class="col-sm-6">
<div class="itflow-glance-item" title="Renewal">
<i class="fas fa-fw fa-calendar-alt"></i>
<?php if ($renewal) { ?>
<span><?= escapeHtml($renewal['date']) ?> <span class="small text-<?= $renewal['tone'] === 'success' ? 'body-secondary' : $renewal['tone'] . '-emphasis' ?>">&middot; <?= escapeHtml($renewal['text']) ?></span></span>
<?php } else { ?>
<span class="text-body-secondary">No renewal date</span>
<?php } ?>
</div>
<?php if ($vendor_name) { ?>
<div class="itflow-glance-item" title="Vendor">
<i class="fas fa-fw fa-building"></i>
<span class="min-w-0">
<?= vendorLink($vendor_id, $license['vendor_name']) ?>
<?php if ($vendor_phone) { ?><span class="text-body-secondary">&middot;</span> <?= $vendor_phone ?><?php } ?>
</span>
</div>
<?php if ($vendor_account) { ?>
<div class="itflow-glance-item" title="Vendor account number">
<i class="fas fa-fw fa-id-card"></i>
<span><span class="font-monospace"><?= $vendor_account ?></span><button type="button" class="itflow-copy clipboardjs" title="Copy account number" data-clipboard-text="<?= $vendor_account ?>"><i class="far fa-copy"></i></button></span>
</div>
<?php } ?>
<?php } ?>
</div>
</div>
<?php if ($can_edit || $software_notes) { ?>
<textarea class="form-control form-control-sm mt-3 itflow-license-notes" rows="2" placeholder="Notes - saves when you click away" aria-label="Notes"
data-software-id="<?= $software_id ?>" data-csrf="<?= escapeHtml($_SESSION['csrf_token'] ?? '') ?>" <?= $can_edit ? '' : 'readonly' ?>><?= $software_notes ?></textarea>
<?php } ?>
<?php if ($quick_tabs) { ?>
<div class="itflow-asset-related itflow-asset-quick-related mt-3" data-license-quick="<?= $software_id ?>">
<ul class="nav nav-pills itflow-asset-pills mb-2" role="tablist">
<?php foreach ($quick_tabs as $tab_key => $tab) { ?>
<li class="nav-item" role="presentation">
<button class="nav-link text-nowrap <?= $tab_key === $first_tab ? 'active' : '' ?>" data-bs-toggle="pill" data-bs-target="#<?= $quick_id ?>-<?= $tab_key ?>" data-license-quick-tab="<?= $tab_key ?>" type="button" role="tab">
<?= $tab['label'] ?> <span class="badge rounded-pill ms-1"><?= $tab['count'] ?></span>
</button>
</li>
<?php } ?>
</ul>
<div class="tab-content">
<?php if ($holders['assets']) { ?>
<div class="tab-pane fade <?= $first_tab === 'devices' ? 'show active' : '' ?>" id="<?= $quick_id ?>-devices" role="tabpanel">
<ul class="list-unstyled mb-0 itflow-links">
<?php foreach ($holders['assets'] as $holder) { ?>
<li class="itflow-link-row">
<span class="itflow-asset-icon itflow-asset-icon-sm itflow-asset-tone-<?= assetTypeTone($holder['asset_type']) ?>" aria-hidden="true"><i class="fas fa-fw fa-<?= getAssetIcon($holder['asset_type']) ?>"></i></span>
<div class="min-w-0 flex-grow-1">
<div class="d-flex"><a class="ajax-modal text-truncate" href="#" data-modal-size="lg" data-modal-url="modals/asset/asset.php?id=<?= intval($holder['asset_id']) ?>"><?= escapeHtml($holder['asset_name']) ?></a></div>
<div class="small text-body-secondary text-truncate"><?= escapeHtml(implode(' · ', array_filter([trim($holder['asset_make'] . ' ' . $holder['asset_model']), $holder['asset_contact_name']]))) ?></div>
</div>
<?php if ($holder['held_keys']) { ?><span class="small text-nowrap"><?= softwareKeyHtml($holder['held_keys'][0]) ?></span><?php } ?>
</li>
<?php } ?>
</ul>
</div>
<?php } ?>
<?php if ($holders['contacts']) { ?>
<div class="tab-pane fade <?= $first_tab === 'users' ? 'show active' : '' ?>" id="<?= $quick_id ?>-users" role="tabpanel">
<ul class="list-unstyled mb-0 itflow-links">
<?php foreach ($holders['contacts'] as $holder) { ?>
<li class="itflow-link-row">
<?= contactAvatar($holder['contact_client_id'], $holder['contact_id'], $holder['contact_name'], $holder['contact_photo'], 'sm') ?>
<div class="min-w-0 flex-grow-1">
<div class="d-flex"><a class="ajax-modal text-truncate" href="#" data-modal-size="lg" data-modal-url="modals/contact/contact.php?id=<?= intval($holder['contact_id']) ?>"><?= escapeHtml($holder['contact_name']) ?></a></div>
<div class="small text-body-secondary text-truncate"><?= escapeHtml(implode(' · ', array_filter([$holder['contact_title'], $holder['contact_email']]))) ?></div>
</div>
<?php if ($holder['held_keys']) { ?><span class="small text-nowrap"><?= softwareKeyHtml($holder['held_keys'][0]) ?></span><?php } ?>
</li>
<?php } ?>
</ul>
</div>
<?php } ?>
<?php if ($keys) { ?>
<div class="tab-pane fade <?= $first_tab === 'keys' ? 'show active' : '' ?>" id="<?= $quick_id ?>-keys" role="tabpanel">
<ul class="list-group list-group-flush">
<?php foreach ($keys as $key) {
$handed = array_merge(array_column($key['assets'], 'asset_name'), array_column($key['contacts'], 'contact_name')); ?>
<li class="list-group-item px-0 d-flex align-items-center justify-content-between gap-3">
<span class="min-w-0"><?= softwareKeyHtml($key['key']) ?></span>
<?php if ($handed) { ?>
<span class="small text-body-secondary text-truncate"><?= escapeHtml(implode(', ', $handed)) ?></span>
<?php } else { ?>
<span class="badge rounded-pill bg-success-subtle text-success-emphasis border border-success-subtle fw-normal">Free</span>
<?php } ?>
</li>
<?php } ?>
</ul>
</div>
<?php } ?>
<?php if ($credentials) { ?>
<div class="tab-pane fade <?= $first_tab === 'credentials' ? 'show active' : '' ?>" id="<?= $quick_id ?>-credentials" role="tabpanel">
<?= credentialListHtml($credentials) ?>
</div>
<?php } ?>
<?php foreach (['documents' => ['document', $documents], 'files' => ['file', $files]] as $pane => [$to, $linked]) {
if (!$linked) {
continue;
} ?>
<div class="tab-pane fade <?= $first_tab === $pane ? 'show active' : '' ?>" id="<?= $quick_id ?>-<?= $pane ?>" role="tabpanel">
<ul class="list-unstyled mb-0 itflow-links">
<?php foreach ($linked as $linked_row) { ?>
<li class="itflow-link-row"><?= linkRowHtml($to, $linked_row, $client_id) ?></li>
<?php } ?>
</ul>
</div>
<?php } ?>
</div>
</div>
<?php } elseif ($can_edit) { ?>
<div class="text-body-secondary small mt-3">Nothing and nobody on this license yet - <a class="ajax-modal" href="#" data-modal-url="modals/software/software_seats.php?id=<?= $software_id ?>">add devices and users</a>.</div>
<?php } ?>
</div>
<div class="modal-footer">
<a href="license.php?client_id=<?= $client_id ?>&software_id=<?= $software_id ?>" class="btn btn-primary text-bold"><i class="fas fa-arrow-right me-2"></i>Open license</a>
<?php if ($can_edit) { ?>
<button type="button" class="btn btn-outline-secondary ajax-modal" data-modal-url="modals/software/software_seats.php?id=<?= $software_id ?>"><i class="fas fa-users me-2"></i>Seats</button>
<button type="button" class="btn btn-outline-secondary ajax-modal" data-modal-size="lg" data-modal-url="modals/software/software_edit.php?id=<?= $software_id ?>"><i class="fas fa-edit me-2"></i>Edit</button>
<?php } ?>
<button type="button" class="btn btn-light" data-bs-dismiss="modal"><i class="fa fa-times me-2"></i>Close</button>
</div>
<script>
(function () {
// Notes save when you click away - scoped to this modal's own textarea
document.querySelectorAll('.modal .itflow-license-notes:not([data-wired])').forEach(function (notes) {
notes.dataset.wired = '1';
var saved = notes.value;
notes.addEventListener('blur', function () {
if (notes.readOnly || notes.value === saved) {
return;
}
saved = notes.value;
itflowPost('ajax.php', {
software_set_notes: 'TRUE',
csrf_token: notes.dataset.csrf,
software_id: notes.dataset.softwareId,
notes: notes.value
});
});
});
// Open on the tab used last time, if this license has one
var key = 'itflow.licenseQuickTab';
document.querySelectorAll('[data-license-quick]:not([data-wired])').forEach(function (box) {
box.dataset.wired = '1';
var remembered = null;
try {
remembered = localStorage.getItem(key);
} catch (e) {}
var tab = remembered && box.querySelector('[data-license-quick-tab="' + remembered + '"]');
if (tab && !tab.classList.contains('active')) {
bootstrap.Tab.getOrCreateInstance(tab).show();
}
box.addEventListener('shown.bs.tab', function (e) {
try {
localStorage.setItem(key, e.target.dataset.licenseQuickTab);
} catch (err) {}
});
});
})();
</script>
<?php
require_once '../../../includes/modal_footer.php';