Files
itflow/agent/credentials.php
johnnyq 234afd25fb Contacts overhaul: rebuilt contacts list with role tabs and counts, avatars, flag pills, hover copy on phone/mobile/email, inline location picker, linked asset/credential/license/ticket chips and tag filter; contact page rebuilt to match the asset page with header actions, Reach/Details/Quick notes cards and tabbed Assets, Credentials, Tickets, Recurring, Licenses, Documents, Files, Services and Notes; add and edit contact share one tabbed form (Details, Roles, Access, Notes) with field icons, role switches, password generator, duplicate email check and photo removal; contact quick look redone to match the asset quick look; contact avatars now used everywhere a contact appears - contact pickers, ticket header, contact card, client replies, ticket list, kanban, assets, clients list, client header, overview key contacts, credentials, documents, services, invoice/quote/statement recipients, license users and global search - with each contact keeping the same colour throughout; ticket contact pickers show the name with title and role underneath; fixes: permission checks on the add contact modal and contact page/list actions, contact location limited to the contact's own client on add/edit/bulk assign, contacts list no longer selects user account columns, archived assets and credentials left off the contact page and quick look, saving a contact with the client portal disabled no longer clears their portal login method, replacing a photo no longer tries to delete a directory, sort column whitelisted, list counts and tags batched instead of per row, recurring ticket menu markup, undefined index notices, contacts list no longer scrolls sideways on phones, Edit Service shows avatars on unselected contacts and assets, license user labels toggle their checkbox; new functions/contact.php helpers (contactAvatar, contactLink, contactFlagPills, contactPhoneLink, contactAvatarData, contactOptionDescription), shared contact_form.php, contact_location quick pick, removed unused assetContactAvatar and clientOverviewPersonInitials
2026-10-14 18:33:18 -04:00

523 lines
31 KiB
PHP

<?php
// Default Column Sortby Filter
$sort = "credential_name";
$order = "ASC";
// If client_id is in URI then show client Side Bar and client header
if (isset($_GET['client_id'])) {
require_once "includes/inc_all_client.php";
$client_query = "AND credential_client_id = $client_id";
$client_url = "client_id=$client_id&";
// Overide Filter Header Archived
if (isset($_GET['archived']) && $_GET['archived'] == 1) {
$archived = 1;
$archive_query = "credential_archived_at IS NOT NULL";
} else {
$archived = 0;
$archive_query = "credential_archived_at IS NULL";
}
// Log when users load the Credentials page
logAudit("Credential", "View", "$session_name viewed the Credentials page for client", $client_id);
} else {
require_once "includes/inc_client_overview_all.php";
$client_query = '';
$client_url = '';
// Overide Filter Header Archived
if (isset($_GET['archived']) && $_GET['archived'] == 1) {
$archived = 1;
$archive_query = "(client_archived_at IS NOT NULL OR credential_archived_at IS NOT NULL)";
} else {
$archived = 0;
$archive_query = "(client_archived_at IS NULL AND credential_archived_at IS NULL)";
}
// Log when users load the Credentials page
logAudit("Credential", "View", "$session_name viewed the All Credentials page");
}
// Perms
enforceUserPermission('module_credential');
// Tags Filter
if (isset($_GET['tags']) && is_array($_GET['tags']) && !empty($_GET['tags'])) {
// Sanitize each element of the tags array
$sanitizedTags = array_map('intval', $_GET['tags']);
// Convert the sanitized tags into a comma-separated string
$tag_filter = implode(",", $sanitizedTags);
$tag_query = "AND tags.tag_id IN ($tag_filter)";
} else {
$tag_filter = 0;
$tag_query = '';
}
if (!$client_url) {
// Client Filter
if (isset($_GET['client']) & !empty($_GET['client'])) {
$client_query = 'AND (credential_client_id = ' . intval($_GET['client']) . ')';
$client = intval($_GET['client']);
} else {
// Default - any
$client_query = '';
$client = '';
}
}
// Location Filter
if ($client_url && isset($_GET['location']) && !empty($_GET['location'])) {
$location_query = 'AND (a.asset_location_id = ' . intval($_GET['location']) . ')';
$location_query_innerjoin = 'INNER JOIN assets a on a.asset_id = c.credential_asset_id ';
$location_filter = intval($_GET['location']);
} else {
// Default - any
$location_query_innerjoin = '';
$location_query = '';
$location_filter = '';
}
$sql = mysqli_query(
$mysqli,
"SELECT SQL_CALC_FOUND_ROWS c.credential_id AS c_credential_id, c.*, credential_tags.*, tags.*, clients.*, contacts.*, assets.*
FROM credentials c
LEFT JOIN credential_tags ON credential_tags.credential_id = c.credential_id
LEFT JOIN tags ON tags.tag_id = credential_tags.tag_id
LEFT JOIN clients ON client_id = credential_client_id
LEFT JOIN contacts ON contact_id = credential_contact_id
LEFT JOIN assets ON asset_id = credential_asset_id
$location_query_innerjoin
WHERE $archive_query
$tag_query
AND (c.credential_name LIKE '%$q%' OR c.credential_description LIKE '%$q%' OR c.credential_uri LIKE '%$q%' OR tag_name LIKE '%$q%' OR client_name LIKE '%$q%')
$location_query
" . clientScopeSql('credential_client_id') . "
$client_query
GROUP BY c.credential_id
ORDER BY c.credential_favorite DESC, $sort $order LIMIT $record_from, $record_to"
);
$num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()"));
?>
<div class="card">
<div class="card-header bg-dark py-2">
<h3 class="card-title mt-2"><i class="fa fa-fw fa-key me-2"></i>Credentials</h3>
<div class="card-tools">
<?php if (lookupUserPermission("module_credential") >= 2) { ?>
<div class="btn-group">
<button type="button" class="btn btn-primary ajax-modal" data-modal-url="modals/credential/credential_add.php?<?= $client_url ?>" <?php if (!isset($_COOKIE['user_encryption_session_key'])) { echo "disabled"; } ?>>
<i class="fas fa-plus me-2"></i>New Credential
</button>
<button type="button" class="btn btn-primary dropdown-toggle dropdown-toggle-split" data-bs-toggle="dropdown"></button>
<div class="dropdown-menu">
<?php if ($client_url) { ?>
<a class="dropdown-item text-dark ajax-modal" href="#"
data-modal-url="modals/credential/credential_import.php?<?= $client_url ?>">
<i class="fa fa-fw fa-upload me-2"></i>Import
</a>
<div class="dropdown-divider"></div>
<?php } ?>
<?php if ($num_rows[0] > 0) { ?>
<a class="dropdown-item text-dark ajax-modal" href="#"
data-modal-url="<?= buildExportModalUrl('modals/credential/credential_export.php', ['client_id', 'client', 'tags', 'archived', 'q']) ?>">
<i class="fa fa-fw fa-download me-2"></i>Export
</a>
<?php } ?>
</div>
</div>
<?php } ?>
</div>
</div>
<div class="card-header py-3">
<form autocomplete="off">
<?php if ($client_url) { ?>
<input type="hidden" name="client_id" value="<?= $client_id ?>">
<?php } ?>
<input type="hidden" name="archived" value="<?= $archived ?>">
<div class="row g-2 align-items-center">
<div class="col-md-4">
<div class="input-group">
<input type="search" class="form-control" name="q" value="<?php if (isset($q)) { echo stripslashes(escapeHtml($q)); } ?>" placeholder="Search Credentials">
<button class="btn btn-dark" title="Search"><i class="fa fa-search"></i></button>
</div>
</div>
<div class="col-md-3">
<div class="input-group">
<select onchange="this.form.submit()" class="form-select select2" name="tags[]" data-placeholder="- Select Tags -" multiple>
<?php
$sql_tags_filter = mysqli_query($mysqli, "
SELECT tags.tag_id, tags.tag_name, MAX(tags.tag_color) AS tag_color, MAX(tags.tag_icon) AS tag_icon
FROM tags
LEFT JOIN credential_tags ON credential_tags.tag_id = tags.tag_id
LEFT JOIN credentials ON credential_tags.credential_id = credentials.credential_id
WHERE tag_type = 4
$client_query OR tags.tag_id IN ($tag_filter)
GROUP BY tags.tag_id
HAVING COUNT(credential_tags.credential_id) > 0 OR tags.tag_id IN ($tag_filter)
");
while ($row = mysqli_fetch_assoc($sql_tags_filter)) {
$tag_id = intval($row['tag_id']);
$tag_name = escapeHtml($row['tag_name']); ?>
<option value="<?= $tag_id ?>" <?php if (isset($_GET['tags']) && is_array($_GET['tags']) && in_array($tag_id, $_GET['tags'])) { echo 'selected'; } ?><?= selectOptionColor($row['tag_color'] ?? '', $row['tag_icon'] ?? '') ?>> <?= $tag_name ?> </option>
<?php } ?>
</select>
</div>
</div>
<?php if ($client_url) { ?>
<div class="col-md-2">
<div class="input-group">
<select class="form-select select2" name="location" onchange="this.form.submit()">
<option value="">- All Asset Locations -</option>
<?php
$sql_locations_filter = mysqli_query($mysqli, "SELECT location_id, location_name FROM locations WHERE location_client_id = $client_id AND location_archived_at IS NULL ORDER BY location_name ASC");
while ($row = mysqli_fetch_assoc($sql_locations_filter)) {
$location_id = intval($row['location_id']);
$location_name = escapeHtml($row['location_name']);
?>
<option <?php if ($location_filter == $location_id) { echo "selected"; } ?> value="<?= $location_id ?>"><?= $location_name ?></option>
<?php
}
?>
</select>
</div>
</div>
<?php } else { ?>
<div class="col-md-2">
<div class="input-group">
<select class="form-select select2" name="client" onchange="this.form.submit()">
<option value="" <?php if ($client == "") { echo "selected"; } ?>>- All Clients -</option>
<?php
$sql_clients_filter = mysqli_query($mysqli, "
SELECT DISTINCT client_id, client_name, client_abbreviation
FROM clients
JOIN credentials ON credential_client_id = client_id
WHERE $archive_query
" . clientScopeSql('clients.client_id') . "
ORDER BY client_name ASC
");
while ($row = mysqli_fetch_assoc($sql_clients_filter)) {
$client_id = intval($row['client_id']);
$client_name = escapeHtml($row['client_name']);
?>
<option <?php if ($client == $client_id) { echo "selected"; } ?> value="<?= $client_id ?>"<?= selectOptionClient($row['client_id'], $row['client_name'], $row['client_abbreviation'] ?? '') ?>><?= $client_name ?></option>
<?php
}
?>
</select>
</div>
</div>
<?php } ?>
<div class="col-md-3">
<div class="btn-group float-end">
<a href="?<?= $client_url ?>&archived=<?php if($archived == 1){ echo 0; } else { echo 1; } ?>"
class="btn btn-<?php if($archived == 1){ echo"primary"; } else { echo "default"; } ?>">
<i class="fa fa-fw fa-archive me-2"></i>Archived
</a>
<div class="dropdown ms-2" id="bulkActionButton" hidden>
<button class="btn btn-secondary dropdown-toggle" type="button" data-bs-toggle="dropdown">
<i class="fas fa-fw fa-layer-group me-2"></i>Bulk Action (<span id="selectedCount">0</span>)
</button>
<div class="dropdown-menu">
<button class="dropdown-item"
type="submit" form="bulkActions" name="bulk_favorite_credentials">
<i class="fas fa-fw fa-star text-warning me-2"></i>Favorite
</button>
<div class="dropdown-divider"></div>
<button class="dropdown-item"
type="submit" form="bulkActions" name="bulk_unfavorite_credentials">
<i class="far fa-fw fa-star me-2"></i>Unfavorite
</button>
<?php if ($archived) { ?>
<div class="dropdown-divider"></div>
<button class="dropdown-item text-info"
type="submit" form="bulkActions" name="bulk_restore_credentials">
<i class="fas fa-fw fa-redo me-2"></i>Restore
</button>
<div class="dropdown-divider"></div>
<button class="dropdown-item text-danger text-bold"
type="submit" form="bulkActions" name="bulk_delete_credentials">
<i class="fas fa-fw fa-trash me-2"></i>Delete
</button>
<?php } else { ?>
<div class="dropdown-divider"></div>
<a class="dropdown-item" href="#" data-tag-picker data-tag-entity="credential" data-tag-bulk data-csrf="<?= $_SESSION['csrf_token'] ?>">
<i class="fas fa-fw fa-tags me-2"></i>Tags
</a>
<div class="dropdown-divider"></div>
<button class="dropdown-item text-danger confirm-link"
type="submit" form="bulkActions" name="bulk_archive_credentials">
<i class="fas fa-fw fa-archive me-2"></i>Archive
</button>
<?php } ?>
</div>
</div>
</div>
</div>
</div>
</form>
</div>
<form id="bulkActions" action="post.php" method="post">
<input type="hidden" name="csrf_token" value="<?= $_SESSION['csrf_token'] ?>">
<div class="table-responsive">
<table class="table table-striped table-borderless table-hover mb-0">
<thead class="text-dark <?php if ($num_rows[0] == 0) { echo "d-none"; } ?> text-nowrap">
<tr>
<td class="checkbox-column border-end">
<div class="form-check">
<input class="form-check-input" id="selectAllCheckbox" type="checkbox" onclick="checkAll(this)">
</div>
</td>
<th>
<a class="text-secondary" href="?<?= $url_query_strings_sort ?>&sort=credential_name&order=<?= $disp ?>">
Name <?php if ($sort == 'credential_name') { echo $order_icon; } ?>
</a>
</th>
<th>Username / ID</th>
<th>Password / Key</th>
<th>OTP</th>
<th>
<a class="text-secondary" href="?<?= $url_query_strings_sort ?>&sort=credential_uri&order=<?= $disp ?>">
URI <?php if ($sort == 'credential_uri') { echo $order_icon; } ?>
</a>
</th>
<th></th>
<?php if (!$client_url) { ?>
<th>
<a class="text-secondary" href="?<?= $url_query_strings_sort ?>&sort=client_name&order=<?= $disp ?>">
Client <?php if ($sort == 'client_name') { echo $order_icon; } ?>
</a>
</th>
<?php } ?>
<th class="text-center">Action</th>
</tr>
</thead>
<tbody>
<?php
while ($row = mysqli_fetch_assoc($sql)) {
$client_id = intval($row['client_id']);
$client_name = escapeHtml($row['client_name']);
$credential_id = intval($row['c_credential_id']);
$credential_name = escapeHtml($row['credential_name']);
$credential_description = escapeHtml($row['credential_description']);
$credential_uri = escapeUrl($row['credential_uri']);
if (empty($credential_uri)) {
$credential_uri_display = "-";
} else {
$credential_uri_display = "<a href='$credential_uri'>" . truncate($credential_uri,40) . "</a><button type='button' class='itflow-copy clipboardjs' title='Copy URI' data-clipboard-text='$credential_uri'><i class='far fa-copy'></i></button>";
}
$credential_uri_2 = escapeUrl($row['credential_uri_2']);
$credential_username = escapeHtml(decryptCredentialEntry($row['credential_username']));
if (empty($credential_username)) {
$credential_username_display = "-";
} else {
$credential_username_display = "$credential_username<button type='button' class='itflow-copy clipboardjs' title='Copy username' data-clipboard-text='$credential_username'><i class='far fa-copy'></i></button>";
}
$credential_otp_secret = escapeHtml($row['credential_otp_secret']);
if (empty($credential_otp_secret)) {
$otp_display = "-";
} else {
$otp_display = "<span onmouseenter='showOTPViaCredentialID($credential_id)'><i class='far fa-clock'></i> <span id='otp_$credential_id'><i>Hover..</i></span></span>";
}
$credential_note = escapeHtml($row['credential_note']);
$credential_created_at = escapeHtml($row['credential_created_at']);
$credential_archived_at = escapeHtml($row['credential_archived_at']);
$credential_favorite = intval($row['credential_favorite']);
$credential_contact_id = intval($row['credential_contact_id']);
$contact_name = escapeHtml($row['contact_name']);
$credential_asset_id = intval($row['credential_asset_id']);
$asset_name = escapeHtml($row['asset_name']);
// Tags
$credential_tags = tagsForRecord('credential', $credential_id);
if ($credential_contact_id) {
// Their avatar, the size of the asset pill beside it
$credential_contact_display = "<a href='#' class='me-2 mb-1 d-inline-flex align-middle ajax-modal' title='$contact_name'
data-modal-size='lg'
data-modal-url='modals/contact/contact.php?id=$credential_contact_id'>"
. contactAvatar($row['credential_client_id'], $credential_contact_id, $row['contact_name'], $row['contact_photo'], 'md') . "</a>";
} else {
$credential_contact_display = '';
}
if ($credential_asset_id) {
$credential_asset_display = "<a href='#' class='me-2 mb-1 badge rounded-pill bg-secondary p-2 ajax-modal' title='$asset_name' data-toggle=''
data-modal-size='lg'
data-modal-url='modals/asset/asset.php?id=$credential_asset_id'>
<i class='fas fa-fw fa-desktop'></i></a>";
} else {
$credential_asset_display = '';
}
// Check if shared
$sql_shared = mysqli_query(
$mysqli,
"SELECT item_active, item_created_at, item_expire_at, item_id, item_key, item_note, item_recipient,
item_related_id, item_type, item_view_limit, item_views FROM shared_items
WHERE item_client_id = $client_id
AND item_active = 1
AND (COALESCE(item_view_limit, 0) = 0 OR item_views < item_view_limit)
AND item_expire_at > NOW()
AND item_type = 'Credential'
AND item_related_id = $credential_id
LIMIT 1"
);
if (mysqli_num_rows($sql_shared) > 0) {
$row = mysqli_fetch_assoc($sql_shared);
$item_id = intval($row['item_id']);
$item_active = escapeHtml($row['item_active']);
$item_key = escapeHtml($row['item_key']);
$item_type = escapeHtml($row['item_type']);
$item_related_id = intval($row['item_related_id']);
$item_note = escapeHtml($row['item_note']);
$item_recipient = escapeHtml($row['item_recipient']);
$item_views = escapeHtml($row['item_views']);
$item_view_limit = escapeHtml($row['item_view_limit']);
$item_created_at = escapeHtml($row['item_created_at']);
$item_expire_at = escapeHtml($row['item_expire_at']);
$item_expire_at_human = timeAgo($row['item_expire_at']);
}
?>
<tr class="<?php if ($credential_favorite) { echo "text-bold"; } ?>">
<td class="checkbox-column bg-light border-end">
<div class="form-check">
<input class="form-check-input bulk-select" type="checkbox" name="credential_ids[]" value="<?= $credential_id ?>">
</div>
</td>
<!-- The whole cell opens the credential (stretched-link); the tag pills sit above it -->
<td class="position-relative">
<div class="d-flex">
<i class="fa fa-fw fa-2x fa-key me-3"></i>
<div class="flex-grow-1">
<a class="text-dark stretched-link ajax-modal" href="#" data-modal-url="modals/credential/credential_edit.php?id=<?= $credential_id ?>"><?= $credential_name ?> <?php if ($credential_favorite) { echo "<i class='fas fa-fw fa-star text-warning' title='Favorite'></i>"; } ?></a>
<div><small class="text-secondary"><?= $credential_description ?></small></div>
<?php if ($credential_tags || lookupUserPermission('module_credential') >= 2) { ?>
<div class="mt-1"><?= tagPills('credential', $credential_id, $credential_tags, "credentials.php?$client_url") ?></div>
<?php } ?>
</div>
</div>
</td>
<td class="text-nowrap"><?= $credential_username_display ?></td>
<td class="text-nowrap">
<button class="btn p-0" aria-label="Show password" type="button" onclick="showPasswordViaCredentialID(this, <?= $credential_id ?>)"><i class="fas fa-2x fa-ellipsis-h text-secondary"></i><i class="fas fa-2x fa-ellipsis-h text-secondary"></i></button><button type="button" class="itflow-copy" title="Copy password" onclick="copyPasswordViaCredentialID(this, <?= $credential_id ?>)"><i class="far fa-copy"></i></button>
</td>
<td class="text-nowrap"><?= $otp_display ?></td>
<td><?= $credential_uri_display ?></td>
<td>
<?= "$credential_contact_display$credential_asset_display" ?>
<?php if (mysqli_num_rows($sql_shared) > 0) { ?>
<div class="d-flex" title="Expires <?= $item_expire_at_human ?>">
<i class="fas fa-link me-2 mt-1"></i>
<div class="flex-grow-1">Shared
<br>
<small class="text-secondary"><?= $item_recipient ?></small>
</div>
</div>
<?php } ?>
</td>
<?php if (!$client_url) { ?>
<td><a href="credentials.php?client_id=<?= $client_id ?>"><?= $client_name ?></a></td>
<?php } ?>
<td class="text-center">
<div class="btn-group">
<?php if ( !empty($credential_uri) || !empty($credential_uri_2) ) { ?>
<div class="dropdown dropstart text-center">
<button class="btn btn-default btn-sm" title="Open link" type="button" data-bs-toggle="dropdown">
<i class="fa fa-fw fa-external-link-alt"></i>
</button>
<div class="dropdown-menu">
<?php if ($credential_uri) { ?>
<a href="<?= $credential_uri ?>" alt="<?= $credential_uri ?>" target="_blank" class="dropdown-item" >
<i class="fa fa-fw fa-external-link-alt"></i> <?= truncate($credential_uri,40) ?>
</a>
<?php } ?>
<?php if ($credential_uri_2) { ?>
<div class="dropdown-divider"></div>
<a href="<?= $credential_uri_2 ?>" target="_blank" class="dropdown-item" >
<i class="fa fa-fw fa-external-link-alt"></i> <?= truncate($credential_uri_2,40) ?>
</a>
<?php } ?>
</div>
</div>
<?php } ?>
<div class="dropdown dropstart text-center">
<button class="btn btn-secondary btn-sm" title="More actions" type="button" data-bs-toggle="dropdown">
<i class="fas fa-ellipsis-h"></i>
</button>
<div class="dropdown-menu">
<a class="dropdown-item ajax-modal" href="#"
data-modal-url="modals/credential/credential_edit.php?id=<?= $credential_id ?>">
<i class="fas fa-fw fa-edit me-2"></i>Edit
</a>
<div class="dropdown-divider"></div>
<?php if ($client_url) { ?>
<a class="dropdown-item" href="#" data-bs-toggle="modal" data-bs-target="#shareModal" onclick="populateShareModal(<?= "$client_id, 'Credential', $credential_id" ?>)">
<i class="fas fa-fw fa-share me-2"></i>Share
</a>
<?php } ?>
<?php if (lookupUserPermission("module_credential") >= 2) { ?>
<?php if ($credential_archived_at) { ?>
<div class="dropdown-divider"></div>
<a class="dropdown-item text-info confirm-link" href="post.php?restore_credential=<?= $credential_id ?>&csrf_token=<?= $_SESSION['csrf_token'] ?>">
<i class="fas fa-fw fa-redo me-2"></i>Restore
</a>
<?php if (lookupUserPermission("module_credential") >= 3) { ?>
<div class="dropdown-divider"></div>
<a class="dropdown-item text-danger text-bold confirm-link" href="post.php?delete_credential=<?= $credential_id ?>&csrf_token=<?= $_SESSION['csrf_token'] ?>">
<i class="fas fa-fw fa-trash me-2"></i>Delete
<?php } ?>
</a>
<?php } else { ?>
<div class="dropdown-divider"></div>
<a class="dropdown-item text-danger confirm-link" href="post.php?archive_credential=<?= $credential_id ?>&csrf_token=<?= $_SESSION['csrf_token'] ?>">
<i class="fas fa-fw fa-archive me-2"></i>Archive
</a>
<?php } ?>
<?php } ?>
</div>
</div>
</div>
</td>
</tr>
<?php
}
?>
</tbody>
</table>
</div>
</form>
<?php require_once "../includes/filter_footer.php"; ?>
</div>
<!-- Include script to get TOTP code via the login ID -->
<script src="js/credential_show_otp_via_id.js"></script>
<script src="js/credential_show_password_via_id.js"></script>
<script src="../js/bulk_actions.js"></script>
<?php
if ($client_url) {
require_once "modals/share_modal.php";
}
require_once "../includes/footer.php";