feat(employees): добавлен функционал отображения LDAP логина в профиле
- Добавлено поле с LDAP логином (sAMAccountName) в профиле сотрудника
- Реализован blur-эффект с раскрытием по клику для безопасности
- Создан API endpoint /api/v1/employees/{id}/ldap-info/ для получения LDAP данных
- Добавлен новый пермишен employees.view_ldap_info
- Стили: SCSS компонент _ldap-login.scss с анимациями и состояниями
- JavaScript: класс LdapLoginField с AJAX запросами и обработкой ошибок
- Доступ только для staff или пользователей с правом view_ldap_info
- Данные запрашиваются из Active Directory on-demand (не хранятся в БД)
This commit is contained in:
parent
da7bf54c6c
commit
c6cd2ba20d
@ -2,12 +2,15 @@
|
||||
from __future__ import annotations
|
||||
# backend/api/v1/employees/views.py
|
||||
|
||||
import logging
|
||||
import traceback
|
||||
from datetime import timedelta
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from common.emails import send_templated_mail
|
||||
from django.conf import settings
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.contrib.auth.models import Group, Permission
|
||||
from django.core.exceptions import FieldError
|
||||
@ -2251,6 +2254,76 @@ class EmployeeViewSet(viewsets.ModelViewSet):
|
||||
{"ok": True, "removed": {"id": sk.id, "name": sk.name}}, status=200
|
||||
)
|
||||
|
||||
@action(detail=True, methods=["get"], url_path="ldap-info")
|
||||
def ldap_info(self, request, pk=None):
|
||||
"""
|
||||
GET /api/v1/employees/{id}/ldap-info/
|
||||
|
||||
Получает LDAP информацию о сотруднике из Active Directory.
|
||||
|
||||
Доступ: только staff или пользователи с правом employees.view_ldap_info
|
||||
|
||||
Возвращает:
|
||||
- sAMAccountName: LDAP логин пользователя
|
||||
- userPrincipalName: полный UPN
|
||||
- displayName: отображаемое имя из LDAP
|
||||
|
||||
Примечания:
|
||||
- Данные запрашиваются напрямую из LDAP, не из базы Django
|
||||
- Требуется активная связь с Active Directory
|
||||
"""
|
||||
from employees.ldap.repositories.ldap_repository import LdapRepository
|
||||
|
||||
# Проверка прав доступа
|
||||
if not (request.user.is_staff or request.user.has_perm('employees.view_ldap_info')):
|
||||
return Response(
|
||||
{"detail": "У вас нет прав для просмотра LDAP информации"},
|
||||
status=status.HTTP_403_FORBIDDEN
|
||||
)
|
||||
|
||||
emp = self.get_object()
|
||||
|
||||
# Проверяем наличие LDAP связи
|
||||
try:
|
||||
ldap_sync = emp.ldap_sync_state
|
||||
if not ldap_sync or not ldap_sync.ldap_dn:
|
||||
return Response(
|
||||
{"detail": "У этого сотрудника нет связи с LDAP"},
|
||||
status=status.HTTP_404_NOT_FOUND
|
||||
)
|
||||
except LdapSyncState.DoesNotExist:
|
||||
return Response(
|
||||
{"detail": "У этого сотрудника нет связи с LDAP"},
|
||||
status=status.HTTP_404_NOT_FOUND
|
||||
)
|
||||
|
||||
# Запрашиваем данные из LDAP
|
||||
try:
|
||||
ldap_repo = LdapRepository()
|
||||
attrs = ldap_repo.read_attrs(
|
||||
ldap_sync.ldap_dn,
|
||||
['sAMAccountName', 'userPrincipalName', 'displayName']
|
||||
)
|
||||
|
||||
if not attrs or not attrs.get('sAMAccountName'):
|
||||
return Response(
|
||||
{"detail": "Не удалось получить LDAP информацию"},
|
||||
status=status.HTTP_404_NOT_FOUND
|
||||
)
|
||||
|
||||
return Response({
|
||||
"sAMAccountName": attrs.get('sAMAccountName'),
|
||||
"userPrincipalName": attrs.get('userPrincipalName'),
|
||||
"displayName": attrs.get('displayName'),
|
||||
}, status=status.HTTP_200_OK)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error fetching LDAP info for employee {emp.id}: {e}", exc_info=True)
|
||||
return Response(
|
||||
{"detail": f"Ошибка при запросе LDAP информации: {str(e)}"},
|
||||
status=status.HTTP_500_INTERNAL_SERVER_ERROR
|
||||
)
|
||||
|
||||
@action(detail=False, methods=["get"], url_path="export-excel")
|
||||
def export_excel(self, request):
|
||||
"""
|
||||
|
||||
17
backend/employees/migrations/0037_alter_employee_options.py
Normal file
17
backend/employees/migrations/0037_alter_employee_options.py
Normal file
@ -0,0 +1,17 @@
|
||||
# Generated by Django 5.2.4 on 2026-02-06 11:18
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('employees', '0036_migrate_role_assignments'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name='employee',
|
||||
options={'ordering': ('last_name', 'first_name'), 'permissions': [('manage_employee_skills', 'Может управлять навыками сотрудников'), ('view_ldap_info', 'Может просматривать LDAP информацию сотрудников')], 'verbose_name': 'Сотрудник', 'verbose_name_plural': 'Сотрудники'},
|
||||
),
|
||||
]
|
||||
@ -212,6 +212,7 @@ class Employee(AbstractUser):
|
||||
]
|
||||
permissions = [
|
||||
("manage_employee_skills", "Может управлять навыками сотрудников"),
|
||||
("view_ldap_info", "Может просматривать LDAP информацию сотрудников"),
|
||||
]
|
||||
|
||||
def clean(self):
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
208
backend/static/js/components/ldapLoginField.js
Normal file
208
backend/static/js/components/ldapLoginField.js
Normal file
@ -0,0 +1,208 @@
|
||||
/**
|
||||
* LDAP Login Field Component
|
||||
* =============================================================================
|
||||
* Компонент для безопасного отображения LDAP логина пользователя
|
||||
* - Blur-эффект по умолчанию
|
||||
* - Раскрытие по клику (только для админов/HR)
|
||||
* - AJAX запрос к API для получения реального sAMAccountName
|
||||
*/
|
||||
|
||||
class LdapLoginField {
|
||||
constructor(fieldElement) {
|
||||
this.field = fieldElement;
|
||||
this.button = this.field.querySelector('.ldap-login-toggle');
|
||||
this.textSpan = this.field.querySelector('.ldap-login-text');
|
||||
this.icon = this.field.querySelector('.ldap-icon');
|
||||
this.employeeId = this.field.dataset.employeeId;
|
||||
|
||||
this.isRevealed = false;
|
||||
this.ldapLogin = null;
|
||||
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
if (!this.button || !this.textSpan || !this.icon) {
|
||||
console.error('LDAP Login Field: Required elements not found');
|
||||
return;
|
||||
}
|
||||
|
||||
this.button.addEventListener('click', () => this.toggleReveal());
|
||||
}
|
||||
|
||||
async toggleReveal() {
|
||||
if (this.isRevealed) {
|
||||
this.hide();
|
||||
} else {
|
||||
await this.reveal();
|
||||
}
|
||||
}
|
||||
|
||||
async reveal() {
|
||||
if (!this.employeeId) {
|
||||
this.showError('ID сотрудника не указан');
|
||||
return;
|
||||
}
|
||||
|
||||
// Уже загружено
|
||||
if (this.ldapLogin) {
|
||||
this.showLogin(this.ldapLogin);
|
||||
return;
|
||||
}
|
||||
|
||||
// Показать состояние загрузки
|
||||
this.field.classList.add('loading');
|
||||
this.icon.classList.remove('bi-lock-fill');
|
||||
this.icon.classList.add('bi-hourglass');
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/v1/employees/${this.employeeId}/ldap-info/`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Requested-With': 'XMLHttpRequest'
|
||||
},
|
||||
credentials: 'same-origin'
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
if (response.status === 403) {
|
||||
throw new Error('У вас нет прав для просмотра LDAP информации');
|
||||
} else if (response.status === 404) {
|
||||
throw new Error('LDAP информация не найдена');
|
||||
} else {
|
||||
throw new Error('Ошибка загрузки LDAP информации');
|
||||
}
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (!data.sAMAccountName) {
|
||||
throw new Error('LDAP логин не найден');
|
||||
}
|
||||
|
||||
this.ldapLogin = data.sAMAccountName;
|
||||
this.showLogin(this.ldapLogin);
|
||||
|
||||
} catch (error) {
|
||||
console.error('LDAP Login Field Error:', error);
|
||||
this.showError(error.message || 'Не удалось загрузить LDAP логин');
|
||||
} finally {
|
||||
this.field.classList.remove('loading');
|
||||
}
|
||||
}
|
||||
|
||||
showLogin(login) {
|
||||
this.textSpan.textContent = login;
|
||||
this.textSpan.classList.remove('blurred');
|
||||
this.textSpan.classList.add('revealed');
|
||||
|
||||
this.icon.classList.remove('bi-lock-fill', 'bi-hourglass');
|
||||
this.icon.classList.add('bi-unlock-fill');
|
||||
|
||||
this.field.classList.add('revealed');
|
||||
this.isRevealed = true;
|
||||
|
||||
// Обновить tooltip
|
||||
this.button.setAttribute('title', 'Нажмите, чтобы скрыть LDAP логин');
|
||||
if (bootstrap && bootstrap.Tooltip) {
|
||||
const tooltip = bootstrap.Tooltip.getInstance(this.button);
|
||||
if (tooltip) {
|
||||
tooltip.dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
hide() {
|
||||
this.textSpan.textContent = '••••••••';
|
||||
this.textSpan.classList.remove('revealed');
|
||||
this.textSpan.classList.add('blurred');
|
||||
|
||||
this.icon.classList.remove('bi-unlock-fill');
|
||||
this.icon.classList.add('bi-lock-fill');
|
||||
|
||||
this.field.classList.remove('revealed');
|
||||
this.isRevealed = false;
|
||||
|
||||
// Обновить tooltip
|
||||
this.button.setAttribute('title', 'Нажмите, чтобы показать LDAP логин');
|
||||
}
|
||||
|
||||
showError(message) {
|
||||
this.field.classList.add('error');
|
||||
this.icon.classList.remove('bi-lock-fill', 'bi-hourglass');
|
||||
this.icon.classList.add('bi-exclamation-triangle-fill');
|
||||
|
||||
// Показать сообщение об ошибке через tooltip или alert
|
||||
if (typeof bootstrap !== 'undefined' && bootstrap.Toast) {
|
||||
this.showToast('Ошибка', message, 'danger');
|
||||
} else {
|
||||
alert(message);
|
||||
}
|
||||
|
||||
// Убрать состояние ошибки через 3 секунды
|
||||
setTimeout(() => {
|
||||
this.field.classList.remove('error');
|
||||
this.icon.classList.remove('bi-exclamation-triangle-fill');
|
||||
this.icon.classList.add('bi-lock-fill');
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
showToast(title, message, type = 'info') {
|
||||
const toastContainer = document.querySelector('.toast-container') ||
|
||||
this.createToastContainer();
|
||||
|
||||
const toastEl = document.createElement('div');
|
||||
toastEl.className = `toast align-items-center text-bg-${type} border-0`;
|
||||
toastEl.setAttribute('role', 'alert');
|
||||
toastEl.setAttribute('aria-live', 'assertive');
|
||||
toastEl.setAttribute('aria-atomic', 'true');
|
||||
|
||||
toastEl.innerHTML = `
|
||||
<div class="d-flex">
|
||||
<div class="toast-body">
|
||||
<strong>${title}:</strong> ${message}
|
||||
</div>
|
||||
<button type="button" class="btn-close btn-close-white me-2 m-auto"
|
||||
data-bs-dismiss="toast" aria-label="Close"></button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
toastContainer.appendChild(toastEl);
|
||||
|
||||
const toast = new bootstrap.Toast(toastEl, { autohide: true, delay: 5000 });
|
||||
toast.show();
|
||||
|
||||
toastEl.addEventListener('hidden.bs.toast', () => {
|
||||
toastEl.remove();
|
||||
});
|
||||
}
|
||||
|
||||
createToastContainer() {
|
||||
const container = document.createElement('div');
|
||||
container.className = 'toast-container position-fixed top-0 end-0 p-3';
|
||||
container.style.zIndex = '1056';
|
||||
document.body.appendChild(container);
|
||||
return container;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Инициализация всех LDAP Login полей на странице
|
||||
*/
|
||||
function initLdapLoginFields() {
|
||||
const fields = document.querySelectorAll('.ldap-login-field');
|
||||
fields.forEach(field => {
|
||||
new LdapLoginField(field);
|
||||
});
|
||||
}
|
||||
|
||||
// Инициализация при загрузке DOM
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', initLdapLoginFields);
|
||||
} else {
|
||||
initLdapLoginFields();
|
||||
}
|
||||
|
||||
// Экспорт для использования в других модулях
|
||||
export { LdapLoginField, initLdapLoginFields };
|
||||
115
backend/static/scss/components/_ldap-login.scss
Normal file
115
backend/static/scss/components/_ldap-login.scss
Normal file
@ -0,0 +1,115 @@
|
||||
// =============================================================================
|
||||
// LDAP Login Field Component
|
||||
// =============================================================================
|
||||
// Скрытое поле с LDAP логином для админов/HR
|
||||
// Использует blur-эффект и раскрывается по клику
|
||||
|
||||
.ldap-login-field {
|
||||
margin: var(--space-2) 0;
|
||||
text-align: center;
|
||||
|
||||
.ldap-login-toggle {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--space-1);
|
||||
padding: var(--space-1) var(--space-2);
|
||||
background: var(--color-surface-secondary);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-md);
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 0.875rem;
|
||||
font-family: var(--font-mono, 'Courier New', monospace);
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
background: var(--color-surface-tertiary);
|
||||
border-color: var(--color-primary);
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.ldap-icon {
|
||||
font-size: 1rem;
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.ldap-login-text {
|
||||
letter-spacing: 2px;
|
||||
transition: filter 0.5s ease, letter-spacing 0.5s ease;
|
||||
|
||||
&.blurred {
|
||||
filter: blur(6px);
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
&.revealed {
|
||||
filter: blur(0);
|
||||
letter-spacing: normal;
|
||||
color: var(--color-primary);
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Состояние загрузки
|
||||
&.loading {
|
||||
.ldap-login-toggle {
|
||||
pointer-events: none;
|
||||
opacity: 0.7;
|
||||
|
||||
.ldap-icon {
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Состояние ошибки
|
||||
&.error {
|
||||
.ldap-login-toggle {
|
||||
border-color: var(--color-danger);
|
||||
background: rgba(var(--bs-danger-rgb), 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
// Раскрытое состояние
|
||||
&.revealed {
|
||||
.ldap-login-toggle {
|
||||
border-color: var(--color-success);
|
||||
background: rgba(var(--bs-success-rgb), 0.05);
|
||||
|
||||
.ldap-icon {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Анимация вращения для загрузки
|
||||
@keyframes spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
// Темная тема
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.ldap-login-field {
|
||||
.ldap-login-toggle {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border-color: rgba(255, 255, 255, 0.1);
|
||||
|
||||
&:hover {
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
border-color: var(--color-primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -69,6 +69,7 @@
|
||||
// Employees компоненты
|
||||
@import 'components/department-controls';
|
||||
@import 'components/team-wheel';
|
||||
@import 'components/ldap-login';
|
||||
|
||||
// Календарь и rightbar
|
||||
@import 'components/rightbar-calendar';
|
||||
|
||||
@ -11,8 +11,11 @@
|
||||
import { initEmployeeForm } from '{% static "js/components/employeeFormHandler.js" %}';
|
||||
import { smoothScrollTo } from '{% static "js/utils/scroll.js" %}';
|
||||
import { initEmployeeProfile } from '{% static "js/components/employeeProfile.js" %}';
|
||||
import { initLdapLoginFields } from '{% static "js/components/ldapLoginField.js" %}';
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// Инициализация LDAP login полей
|
||||
initLdapLoginFields();
|
||||
// 0. Загрузка данных профиля через AJAX (аватар и посты с правильными URL)
|
||||
const employeeId = '{{ emp.id }}';
|
||||
if (employeeId) {
|
||||
|
||||
@ -66,6 +66,17 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{# LDAP Login (только для админов/HR) #}
|
||||
{% if request.user.is_staff or perms.employees.view_ldap_info %}
|
||||
<div class="ldap-login-field" id="ldapLoginField" data-employee-id="{{ emp.id }}">
|
||||
<button type="button" class="ldap-login-toggle" data-bs-toggle="tooltip"
|
||||
data-bs-placement="top" title="Нажмите, чтобы показать LDAP логин">
|
||||
<i class="bi-lock-fill ldap-icon"></i>
|
||||
<span class="ldap-login-text blurred">••••••••</span>
|
||||
</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="ios-role">
|
||||
{% if emp.position.name %}
|
||||
{{ emp.position.name }}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user