feat: service credentials (login/password) on dashboard cards with copy button
This commit is contained in:
@@ -123,7 +123,7 @@
|
||||
<input class="list-search" id="web_search" placeholder="Поиск WEB сервиса..." oninput="filterList('web_search', '#web_list .web-item')" />
|
||||
<div class="list-box" id="web_list">
|
||||
{% for s in web_services %}
|
||||
<button class="list-item service-row web-item" data-service-id="{{s.id}}" data-filter="{{(s.name ~ ' ' ~ s.slug)|lower}}" onclick='selectWebService({{s.id}}, {{s.name|tojson}}, {{s.slug|tojson}}, {{s.target|tojson}}, {{s.comment|tojson}}, {{s.icon_path|tojson}}, {{s.active|tojson}})'>
|
||||
<button class="list-item service-row web-item" data-service-id="{{s.id}}" data-filter="{{(s.name ~ ' ' ~ s.slug)|lower}}" onclick='selectWebService({{s.id}}, {{s.name|tojson}}, {{s.slug|tojson}}, {{s.target|tojson}}, {{s.comment|tojson}}, {{s.icon_path|tojson}}, {{s.active|tojson}}, {{s.svc_login|tojson}}, {{s.svc_password|tojson}})'>
|
||||
<img class="service-thumb" src="{{ s.icon_path or '/static/service-placeholder.svg' }}" alt="icon" />
|
||||
<div>
|
||||
<div>{{s.name}}</div>
|
||||
@@ -158,6 +158,16 @@
|
||||
<textarea id="w_comment" placeholder="Коротко: что это за сервис"></textarea>
|
||||
</label>
|
||||
|
||||
<label class="field-col">
|
||||
<span>Логин сервиса (показывается на карточке)</span>
|
||||
<input id="w_svc_login" placeholder="Например: admin" />
|
||||
</label>
|
||||
|
||||
<label class="field-col">
|
||||
<span>Пароль сервиса (показывается на карточке)</span>
|
||||
<input id="w_svc_password" placeholder="Пароль для входа в сервис" />
|
||||
</label>
|
||||
|
||||
<label class="field-col">
|
||||
<span>Статус</span>
|
||||
<select id="w_active"><option value="true">active</option><option value="false">inactive</option></select>
|
||||
@@ -231,6 +241,8 @@
|
||||
<label class="field-col">
|
||||
<span>Описание для пользователя</span>
|
||||
<textarea id="new_w_comment" placeholder="Коротко: что это за сервис"></textarea>
|
||||
<input id="new_w_svc_login" placeholder="Логин сервиса (необязательно)" />
|
||||
<input id="new_w_svc_password" placeholder="Пароль сервиса (необязательно)" />
|
||||
</label>
|
||||
|
||||
<label class="field-col">
|
||||
@@ -257,7 +269,7 @@
|
||||
<input class="list-search" id="rdp_search" placeholder="Поиск RDP сервиса..." oninput="filterList('rdp_search', '#rdp_list .rdp-item')" />
|
||||
<div class="list-box" id="rdp_list">
|
||||
{% for s in rdp_services %}
|
||||
<button class="list-item service-row rdp-item" data-service-id="{{s.id}}" data-filter="{{(s.name ~ ' ' ~ s.slug)|lower}}" onclick='selectRdpService({{s.id}}, {{s.name|tojson}}, {{s.slug|tojson}}, {{s.target|tojson}}, {{s.comment|tojson}}, {{s.icon_path|tojson}}, {{s.active|tojson}}, {{s.warm_pool_size}})'>
|
||||
<button class="list-item service-row rdp-item" data-service-id="{{s.id}}" data-filter="{{(s.name ~ ' ' ~ s.slug)|lower}}" onclick='selectRdpService({{s.id}}, {{s.name|tojson}}, {{s.slug|tojson}}, {{s.target|tojson}}, {{s.comment|tojson}}, {{s.icon_path|tojson}}, {{s.active|tojson}}, {{s.warm_pool_size}}, {{s.svc_login|tojson}}, {{s.svc_password|tojson}})'>
|
||||
<img class="service-thumb" src="{{ s.icon_path or '/static/service-placeholder.svg' }}" alt="icon" />
|
||||
<div>
|
||||
<div>{{s.name}}</div>
|
||||
@@ -288,6 +300,8 @@
|
||||
</select>
|
||||
<input id="r_target" placeholder="Собранный target (авто)" />
|
||||
<textarea id="r_comment" placeholder="Описание для пользователя"></textarea>
|
||||
<input id="r_svc_login" placeholder="Логин сервиса (показывается на карточке)" />
|
||||
<input id="r_svc_password" placeholder="Пароль сервиса (показывается на карточке)" />
|
||||
<input id="r_pool" type="number" min="0" placeholder="Количество заранее прогретых слотов" />
|
||||
<select id="r_active"><option value="true">active</option><option value="false">inactive</option></select>
|
||||
</div>
|
||||
@@ -371,6 +385,8 @@
|
||||
</select>
|
||||
<input id="new_r_target" placeholder="Собранный target (авто)" />
|
||||
<textarea id="new_r_comment" placeholder="Описание для пользователя"></textarea>
|
||||
<input id="new_r_svc_login" placeholder="Логин сервиса (необязательно)" />
|
||||
<input id="new_r_svc_password" placeholder="Пароль сервиса (необязательно)" />
|
||||
<input id="new_r_pool" type="number" min="0" value="1" placeholder="Количество прогретых слотов" />
|
||||
<select id="new_r_active"><option value="true">active</option><option value="false">inactive</option></select>
|
||||
</div>
|
||||
@@ -709,12 +725,14 @@
|
||||
location.reload();
|
||||
}
|
||||
|
||||
function selectWebService(id, name, slug, target, comment, iconPath, active) {
|
||||
function selectWebService(id, name, slug, target, comment, iconPath, active, svcLogin, svcPassword) {
|
||||
document.getElementById('w_id').value = id;
|
||||
document.getElementById('w_name').value = name;
|
||||
document.getElementById('w_slug').value = slug;
|
||||
document.getElementById('w_target').value = target;
|
||||
document.getElementById('w_comment').value = comment || '';
|
||||
document.getElementById('w_svc_login').value = svcLogin || '';
|
||||
document.getElementById('w_svc_password').value = svcPassword || '';
|
||||
document.getElementById('w_active').value = String(active);
|
||||
document.getElementById('w_icon_preview').src = iconPath || placeholderIcon;
|
||||
setCategoryChecks('.w_cat', serviceCategoryMap[id] || []);
|
||||
@@ -737,6 +755,8 @@
|
||||
type: 'WEB',
|
||||
target: document.getElementById('new_w_target').value,
|
||||
comment: document.getElementById('new_w_comment').value,
|
||||
svc_login: document.getElementById('new_w_svc_login').value,
|
||||
svc_password: document.getElementById('new_w_svc_password').value,
|
||||
category_ids: checkedCategoryIds('.new_w_cat'),
|
||||
active: document.getElementById('new_w_active').value === 'true',
|
||||
});
|
||||
@@ -753,6 +773,8 @@
|
||||
type: 'WEB',
|
||||
target: document.getElementById('w_target').value,
|
||||
comment: document.getElementById('w_comment').value,
|
||||
svc_login: document.getElementById('w_svc_login').value,
|
||||
svc_password: document.getElementById('w_svc_password').value,
|
||||
category_ids: checkedCategoryIds('.w_cat'),
|
||||
active: document.getElementById('w_active').value === 'true',
|
||||
});
|
||||
@@ -760,7 +782,7 @@
|
||||
}
|
||||
|
||||
function clearWebForm() {
|
||||
['w_id','w_name','w_slug','w_target','w_comment'].forEach(id => document.getElementById(id).value = '');
|
||||
['w_id','w_name','w_slug','w_target','w_comment','w_svc_login','w_svc_password'].forEach(id => document.getElementById(id).value = '');
|
||||
document.getElementById('w_active').value = 'true';
|
||||
setCategoryChecks('.w_cat', []);
|
||||
document.getElementById('w_icon_preview').src = placeholderIcon;
|
||||
@@ -843,7 +865,7 @@
|
||||
location.reload();
|
||||
}
|
||||
|
||||
function selectRdpService(id, name, slug, target, comment, iconPath, active, pool) {
|
||||
function selectRdpService(id, name, slug, target, comment, iconPath, active, pool, svcLogin, svcPassword) {
|
||||
const cfg = parseRdpTarget(target);
|
||||
document.getElementById('r_id').value = id;
|
||||
document.getElementById('r_name').value = name;
|
||||
@@ -854,6 +876,8 @@
|
||||
document.getElementById('r_domain').value = cfg.domain;
|
||||
document.getElementById('r_sec').value = cfg.sec;
|
||||
document.getElementById('r_comment').value = comment || '';
|
||||
document.getElementById('r_svc_login').value = svcLogin || '';
|
||||
document.getElementById('r_svc_password').value = svcPassword || '';
|
||||
document.getElementById('r_active').value = String(active);
|
||||
document.getElementById('r_pool').value = pool;
|
||||
setCategoryChecks('.r_cat', serviceCategoryMap[id] || []);
|
||||
@@ -873,6 +897,8 @@
|
||||
type: 'RDP',
|
||||
target,
|
||||
comment: document.getElementById('new_r_comment').value,
|
||||
svc_login: document.getElementById('new_r_svc_login').value,
|
||||
svc_password: document.getElementById('new_r_svc_password').value,
|
||||
category_ids: checkedCategoryIds('.new_r_cat'),
|
||||
warm_pool_size: parseInt(document.getElementById('new_r_pool').value || '0', 10),
|
||||
active: document.getElementById('new_r_active').value === 'true',
|
||||
@@ -890,6 +916,8 @@
|
||||
type: 'RDP',
|
||||
target,
|
||||
comment: document.getElementById('r_comment').value,
|
||||
svc_login: document.getElementById('r_svc_login').value,
|
||||
svc_password: document.getElementById('r_svc_password').value,
|
||||
category_ids: checkedCategoryIds('.r_cat'),
|
||||
warm_pool_size: parseInt(document.getElementById('r_pool').value || '0', 10),
|
||||
active: document.getElementById('r_active').value === 'true',
|
||||
@@ -898,7 +926,7 @@
|
||||
}
|
||||
|
||||
function clearRdpForm() {
|
||||
['r_id','r_name','r_slug','r_target','r_host','r_port','r_domain','r_comment','r_pool'].forEach(id => document.getElementById(id).value = '');
|
||||
['r_id','r_name','r_slug','r_target','r_host','r_port','r_domain','r_comment','r_pool','r_svc_login','r_svc_password'].forEach(id => document.getElementById(id).value = '');
|
||||
document.getElementById('rdp_slots_box').style.display = 'none';
|
||||
document.getElementById('r_sec').value = '';
|
||||
document.getElementById('r_active').value = 'true';
|
||||
|
||||
@@ -77,23 +77,43 @@
|
||||
<section class="grid service-grid">
|
||||
{% for service in services %}
|
||||
{% set svc_cats = service_categories.get(service.id, []) %}
|
||||
<a class="tile" href="/go/{{ service.slug }}">
|
||||
<div class="tile-icon-box">
|
||||
<img class="tile-icon" src="{{ service.icon_path or '/static/service-placeholder.svg' }}" alt="icon" />
|
||||
</div>
|
||||
<h3>{{ service.name }}</h3>
|
||||
<p>Открыть сервис</p>
|
||||
{% if service.comment %}
|
||||
<small class="tile-comment">{{ service_comment_html.get(service.id, '') }}</small>
|
||||
{% endif %}
|
||||
{% if svc_cats %}
|
||||
<div class="service-categories">
|
||||
{% for category in svc_cats %}
|
||||
<span class="service-cat-badge">{{ category.name }}</span>
|
||||
{% endfor %}
|
||||
<div class="tile-wrap">
|
||||
<a class="tile" href="/go/{{ service.slug }}">
|
||||
<div class="tile-icon-box">
|
||||
<img class="tile-icon" src="{{ service.icon_path or '/static/service-placeholder.svg' }}" alt="icon" />
|
||||
</div>
|
||||
<h3>{{ service.name }}</h3>
|
||||
<p>Открыть сервис</p>
|
||||
{% if service.comment %}
|
||||
<small class="tile-comment">{{ service_comment_html.get(service.id, '') }}</small>
|
||||
{% endif %}
|
||||
{% if svc_cats %}
|
||||
<div class="service-categories">
|
||||
{% for category in svc_cats %}
|
||||
<span class="service-cat-badge">{{ category.name }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</a>
|
||||
{% if service.svc_login or service.svc_password %}
|
||||
<div class="svc-credentials">
|
||||
{% if service.svc_login %}
|
||||
<div class="svc-cred-row">
|
||||
<span class="svc-cred-label">Логин</span>
|
||||
<span class="svc-cred-value">{{ service.svc_login }}</span>
|
||||
<button class="svc-cred-copy" type="button" data-copy="{{ service.svc_login }}" title="Копировать логин"></button>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if service.svc_password %}
|
||||
<div class="svc-cred-row">
|
||||
<span class="svc-cred-label">Пароль</span>
|
||||
<span class="svc-cred-value svc-cred-masked">{{ service.svc_password }}</span>
|
||||
<button class="svc-cred-copy" type="button" data-copy="{{ service.svc_password }}" title="Копировать пароль"></button>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</a>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="tile">
|
||||
{% if selected_category_slug %}
|
||||
@@ -194,5 +214,21 @@
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
<script>
|
||||
document.querySelectorAll('.svc-cred-copy').forEach(btn => {
|
||||
btn.addEventListener('click', async (e) => {
|
||||
e.preventDefault(); e.stopPropagation();
|
||||
const text = btn.dataset.copy;
|
||||
try { await navigator.clipboard.writeText(text); } catch(_) {
|
||||
const ta = document.createElement('textarea');
|
||||
ta.value = text; ta.style.position='fixed'; ta.style.opacity='0';
|
||||
document.body.appendChild(ta); ta.select();
|
||||
document.execCommand('copy'); document.body.removeChild(ta);
|
||||
}
|
||||
btn.classList.add('copied');
|
||||
setTimeout(() => btn.classList.remove('copied'), 1500);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user