81dee5cba2
- 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>
204 lines
17 KiB
HTML
204 lines
17 KiB
HTML
<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>
|