Files
ACORE/templates/pages/ad_mapping.html
T
lokses 81dee5cba2 Интеграции AD/Mattermost: OU-управление, импорт, шифрование токенов
- ad_connector: страницы управления OU и импорта из AD, поле ad_ou_dn
  в Directory, ADSyncLog; расширение sync-сервиса и клиента
- mattermost_connector: personal access token шифруется (Fernet,
  token_service), бот-сервис, команды создания бота и синхронизации;
  публикация постов в каналы. Тесты (19)
- core: ConnectorConfig, Directory (ad_ou_dn), обновления админки

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-22 17:32:26 +03:00

204 lines
17 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<div class="page-content" id="ad-mapping-page">
<div style="display:flex; align-items:center; justify-content:space-between; margin-bottom:1rem; gap:1rem; flex-wrap:wrap;">
<div>
<h1 style="font-size:1.25rem; font-weight:500; color:var(--color-foreground);">Зіставлення з Active Directory</h1>
<p style="font-size:0.875rem; color:var(--color-muted-foreground); margin-top:0.25rem;">Зв'язок офісних співробітників з обліковими записами домену</p>
</div>
<a href="#" class="btn btn-primary"
hx-get="/ad-mapping/"
hx-target="#main-content"
hx-push-url="/ad-mapping/"
hx-indicator="#loader">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M21 12a9 9 0 1 1-9-9c2.52 0 4.93 1 6.74 2.74L21 8"/><path d="M21 3v5h-5"/>
</svg>
Оновити
</a>
</div>
{% include "partials/ad_connector_tabs.html" with active="mapping" %}
<form method="get" hx-get="/ad-mapping/" hx-target="#main-content" hx-push-url="true" hx-indicator="#loader"
style="display:flex; align-items:center; gap:0.5rem; margin-bottom:1.5rem;">
<input type="text" name="q" value="{{ query }}" placeholder="Пошук в AD за логіном, ПІБ або email…"
class="input" style="max-width:24rem;">
<button type="submit" class="btn btn-secondary" style="font-size:0.8125rem;">Знайти</button>
{% if query %}
<a href="#" class="btn btn-ghost" style="font-size:0.8125rem;"
hx-get="/ad-mapping/" hx-target="#main-content" hx-push-url="/ad-mapping/" hx-indicator="#loader">Скинути</a>
{% endif %}
</form>
{% if ad_error %}
<div style="background:color-mix(in srgb, var(--color-destructive) 8%, transparent); border:1px solid color-mix(in srgb, var(--color-destructive) 30%, transparent); border-radius:0.75rem; padding:1rem 1.25rem; margin-bottom:1.5rem; display:flex; align-items:center; gap:0.75rem;">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="var(--color-destructive)" stroke-width="2" style="flex-shrink:0;">
<circle cx="12" cy="12" r="10"/><line x1="12" x2="12" y1="8" y2="12"/><line x1="12" x2="12.01" y1="16" y2="16"/>
</svg>
<div style="font-size:0.875rem; color:var(--color-destructive);">{{ ad_error }}</div>
</div>
{% endif %}
<!-- Статистика -->
<div style="display:grid; grid-template-columns:repeat(auto-fit, minmax(11rem, 1fr)); gap:1rem; margin-bottom:1.5rem;">
<div style="background:var(--color-card); border:1px solid var(--color-border); border-radius:0.75rem; padding:1rem 1.25rem;">
<div style="font-size:0.8125rem; color:var(--color-muted-foreground);">Офісних співробітників</div>
<div style="font-size:1.5rem; font-weight:600; color:var(--color-card-foreground); margin-top:0.25rem;">{{ employees_total }}</div>
</div>
<div style="background:var(--color-card); border:1px solid var(--color-border); border-radius:0.75rem; padding:1rem 1.25rem;">
<div style="font-size:0.8125rem; color:var(--color-muted-foreground);">Зв'язано з AD</div>
<div style="font-size:1.5rem; font-weight:600; color:#16a34a; margin-top:0.25rem;">{{ linked_count }}</div>
</div>
<div style="background:var(--color-card); border:1px solid var(--color-border); border-radius:0.75rem; padding:1rem 1.25rem;">
<div style="font-size:0.8125rem; color:var(--color-muted-foreground);">Без облікового запису</div>
<div style="font-size:1.5rem; font-weight:600; color:var(--color-primary); margin-top:0.25rem;">{{ unlinked_count }}</div>
</div>
<div style="background:var(--color-card); border:1px solid var(--color-border); border-radius:0.75rem; padding:1rem 1.25rem;">
<div style="font-size:0.8125rem; color:var(--color-muted-foreground);">{% if query %}Знайдено за запитом{% else %}Записів у домені{% endif %}</div>
<div style="font-size:1.5rem; font-weight:600; color:var(--color-card-foreground); margin-top:0.25rem;">{{ ad_total }}</div>
</div>
</div>
<!-- Таблица сопоставления -->
<div style="background:var(--color-card); border-radius:0.75rem; border:1px solid var(--color-border); box-shadow:0 1px 2px rgb(0 0 0 / 0.05); margin-bottom:1.5rem;">
<div style="padding:1.25rem 1.5rem; border-bottom:1px solid var(--color-border);">
<h3 style="font-weight:600; color:var(--color-card-foreground); font-size:0.9375rem;">Співробітники офісу</h3>
<p style="font-size:0.8125rem; color:var(--color-muted-foreground); margin-top:0.125rem;">Зіставлення з обліковими записами Active Directory</p>
</div>
<div style="overflow-x:auto;">
<table style="width:100%; border-collapse:collapse; font-size:0.875rem;">
<thead>
<tr style="border-bottom:1px solid var(--color-border);">
<th style="padding:0.75rem 1rem; text-align:left; color:var(--color-muted-foreground); font-weight:600; font-size:0.8125rem;">Співробітник</th>
<th style="padding:0.75rem 1rem; text-align:left; color:var(--color-muted-foreground); font-weight:600; font-size:0.8125rem;">Статус</th>
<th style="padding:0.75rem 1rem; text-align:left; color:var(--color-muted-foreground); font-weight:600; font-size:0.8125rem;">Обліковий запис AD</th>
<th style="padding:0.75rem 1rem; text-align:right; color:var(--color-muted-foreground); font-weight:600; font-size:0.8125rem;">Дії</th>
</tr>
</thead>
<tbody>
{% for employee in employees %}
<tr style="border-bottom:1px solid var(--color-border); transition:background 150ms;" class="tr-hover">
<td style="padding:0.875rem 1rem; vertical-align:middle;">
<div style="font-weight:500; color:var(--color-card-foreground);">{{ employee.get_full_name }}</div>
<div style="font-size:0.8125rem; color:var(--color-muted-foreground); margin-top:0.125rem;">
{{ employee.post.name|default:"—" }} · {{ employee.email }}
</div>
</td>
<td style="padding:0.875rem 1rem; vertical-align:middle; white-space:nowrap;">
{% if employee.dn_missing %}
<span style="display:inline-flex; align-items:center; padding:0.125rem 0.625rem; border-radius:9999px; font-size:0.75rem; font-weight:500; background:color-mix(in srgb, var(--color-destructive) 10%, transparent); color:var(--color-destructive);">Не знайдено в AD</span>
{% elif employee.ad_user_dn %}
{% if employee.ad_sync_status == 'synced' %}
<span style="display:inline-flex; align-items:center; padding:0.125rem 0.625rem; border-radius:9999px; font-size:0.75rem; font-weight:500; background:rgba(22,163,74,0.1); color:#16a34a;">Зв'язано</span>
{% elif employee.ad_sync_status == 'error' %}
<span title="{{ employee.ad_sync_error }}" style="display:inline-flex; align-items:center; padding:0.125rem 0.625rem; border-radius:9999px; font-size:0.75rem; font-weight:500; background:color-mix(in srgb, var(--color-destructive) 10%, transparent); color:var(--color-destructive);">Помилка</span>
{% else %}
<span style="display:inline-flex; align-items:center; padding:0.125rem 0.625rem; border-radius:9999px; font-size:0.75rem; font-weight:500; background:rgba(202,138,4,0.12); color:#ca8a04;">Очікує синхронізації</span>
{% endif %}
{% elif employee.suggestion %}
<span style="display:inline-flex; align-items:center; padding:0.125rem 0.625rem; border-radius:9999px; font-size:0.75rem; font-weight:500; background:rgba(37,99,235,0.1); color:#2563eb;">Знайдено збіг ({{ employee.suggestion_reason }})</span>
{% else %}
<span style="display:inline-flex; align-items:center; padding:0.125rem 0.625rem; border-radius:9999px; font-size:0.75rem; font-weight:500; background:var(--color-muted); color:var(--color-muted-foreground);">Не зв'язано</span>
{% endif %}
</td>
<td style="padding:0.875rem 1rem; vertical-align:middle;">
{% if employee.ad_user_dn %}
{% if employee.ad_account %}
<div style="font-weight:500; color:var(--color-card-foreground);">{{ employee.ad_account.sAMAccountName }}</div>
<div style="font-size:0.75rem; color:var(--color-muted-foreground); margin-top:0.125rem; max-width:24rem; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;" title="{{ employee.ad_user_dn }}">{{ employee.ad_user_dn }}</div>
{% else %}
<div style="font-size:0.75rem; color:var(--color-muted-foreground); max-width:24rem; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;" title="{{ employee.ad_user_dn }}">{{ employee.ad_user_dn }}</div>
{% endif %}
{% elif not ad_error %}
<form hx-post="/ad-mapping/" hx-target="#main-content" hx-indicator="#loader" style="display:flex; align-items:center; gap:0.5rem; margin:0;">
{% csrf_token %}
<input type="hidden" name="action" value="link">
<input type="hidden" name="employee_id" value="{{ employee.pk }}">
<select name="dn" class="input" style="font-size:0.8125rem; padding:0.3rem 0.5rem; height:auto; max-width:20rem;">
<option value="">— оберіть запис —</option>
{% for ad_user in unmatched_ad_users %}
<option value="{{ ad_user.dn }}" {% if employee.suggestion and employee.suggestion.dn == ad_user.dn %}selected{% endif %}>
{{ ad_user.sAMAccountName }}{% if ad_user.displayName %} — {{ ad_user.displayName }}{% endif %}
</option>
{% endfor %}
</select>
<button type="submit" class="btn btn-primary" style="font-size:0.8125rem; padding:0.3rem 0.75rem;">Зв'язати</button>
</form>
{% else %}
<span style="color:var(--color-muted-foreground); font-size:0.8125rem;">—</span>
{% endif %}
</td>
<td style="padding:0.875rem 1rem; vertical-align:middle; text-align:right; white-space:nowrap;">
{% if employee.ad_user_dn %}
<form hx-post="/ad-mapping/" hx-target="#main-content" hx-indicator="#loader"
hx-confirm="Відв'язати {{ employee.get_full_name }} від облікового запису AD? Сам запис у домені не змінюється."
style="display:inline; margin:0;">
{% csrf_token %}
<input type="hidden" name="action" value="unlink">
<input type="hidden" name="employee_id" value="{{ employee.pk }}">
<button type="submit" class="btn btn-ghost" style="font-size:0.8125rem; padding:0.3rem 0.75rem; color:var(--color-destructive);">Відв'язати</button>
</form>
{% elif not ad_error %}
<form hx-post="/ad-mapping/" hx-target="#main-content" hx-indicator="#loader"
hx-confirm="Створити обліковий запис у домені для {{ employee.get_full_name }}?"
style="display:inline; margin:0;">
{% csrf_token %}
<input type="hidden" name="action" value="create">
<input type="hidden" name="employee_id" value="{{ employee.pk }}">
<button type="submit" class="btn btn-ghost" style="font-size:0.8125rem; padding:0.3rem 0.75rem;">Створити в AD</button>
</form>
{% endif %}
</td>
</tr>
{% empty %}
<tr>
<td colspan="4" style="padding:2rem; text-align:center; color:var(--color-muted-foreground);">Активних офісних співробітників не знайдено</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% include "partials/pagination.html" with page=employees base_url="/ad-mapping/" page_param="emp_page" size_param="emp_per_page" item_label="співробітників" %}
</div>
<!-- Учётки AD без сотрудника -->
{% if unmatched_ad_users %}
<div style="background:var(--color-card); border-radius:0.75rem; border:1px solid var(--color-border); box-shadow:0 1px 2px rgb(0 0 0 / 0.05);">
<div style="padding:1.25rem 1.5rem; border-bottom:1px solid var(--color-border);">
<h3 style="font-weight:600; color:var(--color-card-foreground); font-size:0.9375rem;">Облікові записи AD без співробітника</h3>
<p style="font-size:0.8125rem; color:var(--color-muted-foreground); margin-top:0.125rem;">{{ unmatched_ad_users_page.paginator.count }} записів у домені не зв'язані з жодним співробітником</p>
</div>
<div style="overflow-x:auto;">
<table style="width:100%; border-collapse:collapse; font-size:0.875rem;">
<thead>
<tr style="border-bottom:1px solid var(--color-border);">
<th style="padding:0.75rem 1rem; text-align:left; color:var(--color-muted-foreground); font-weight:600; font-size:0.8125rem;">Логін</th>
<th style="padding:0.75rem 1rem; text-align:left; color:var(--color-muted-foreground); font-weight:600; font-size:0.8125rem;">Ім'я</th>
<th style="padding:0.75rem 1rem; text-align:left; color:var(--color-muted-foreground); font-weight:600; font-size:0.8125rem;">Email</th>
<th style="padding:0.75rem 1rem; text-align:left; color:var(--color-muted-foreground); font-weight:600; font-size:0.8125rem;">Стан</th>
</tr>
</thead>
<tbody>
{% for ad_user in unmatched_ad_users_page %}
<tr style="border-bottom:1px solid var(--color-border);" class="tr-hover">
<td style="padding:0.75rem 1rem; font-weight:500; color:var(--color-card-foreground);">{{ ad_user.sAMAccountName|default:"—" }}</td>
<td style="padding:0.75rem 1rem;">{{ ad_user.displayName|default:ad_user.cn|default:"—" }}</td>
<td style="padding:0.75rem 1rem; color:var(--color-muted-foreground);">{{ ad_user.mail|default:"—" }}</td>
<td style="padding:0.75rem 1rem;">
{% if ad_user.is_disabled %}
<span style="display:inline-flex; align-items:center; padding:0.125rem 0.625rem; border-radius:9999px; font-size:0.75rem; font-weight:500; background:var(--color-muted); color:var(--color-muted-foreground);">Відключений</span>
{% else %}
<span style="display:inline-flex; align-items:center; padding:0.125rem 0.625rem; border-radius:9999px; font-size:0.75rem; font-weight:500; background:rgba(22,163,74,0.1); color:#16a34a;">Активний</span>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% include "partials/pagination.html" with page=unmatched_ad_users_page base_url="/ad-mapping/" page_param="ad_page" size_param="ad_per_page" item_label="записів" %}
</div>
{% endif %}
</div>