refactor(employees): упрощена логика прав для LDAP endpoint
- Использована встроенная система пермишенов DRF вместо ручной проверки - Добавлен ldap_info в required_perms_by_action с правом view_ldap_info - Добавлен ldap_info в get_permissions() для применения IsSelfOrStaff - Удалена избыточная ручная проверка прав внутри метода ldap_info() - Проверка прав теперь выполняется автоматически через get_object() - Пользователь может видеть свой LDAP логин (IsSelfOrStaff) - Админы и пользователи с view_ldap_info могут видеть любые логины
This commit is contained in:
parent
c6cd2ba20d
commit
17c4e506cf
@ -1,6 +1,5 @@
|
||||
# flake8: noqa
|
||||
from __future__ import annotations
|
||||
# backend/api/v1/employees/views.py
|
||||
|
||||
import logging
|
||||
import traceback
|
||||
@ -10,6 +9,9 @@ from typing import Any, Dict, List, Optional
|
||||
from common.emails import send_templated_mail
|
||||
from django.conf import settings
|
||||
|
||||
# backend/api/v1/employees/views.py
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.contrib.auth.models import Group, Permission
|
||||
@ -69,11 +71,11 @@ from employees.utils import (
|
||||
)
|
||||
from rest_framework import filters, status, viewsets
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.exceptions import ValidationError
|
||||
from rest_framework.parsers import FormParser, JSONParser, MultiPartParser
|
||||
from rest_framework.permissions import AllowAny, IsAuthenticated
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.views import APIView
|
||||
from rest_framework.exceptions import ValidationError
|
||||
|
||||
from ..permissions import (
|
||||
AdminOrActionOrModelPerms,
|
||||
@ -1554,6 +1556,7 @@ class EmployeeViewSet(viewsets.ModelViewSet):
|
||||
required_perms_by_action = {
|
||||
"add_skill": "employees.manage_employee_skills",
|
||||
"remove_skill": "employees.manage_employee_skills",
|
||||
"ldap_info": "employees.view_ldap_info", # Для просмотра чужих LDAP логинов
|
||||
}
|
||||
|
||||
def get_permissions(self):
|
||||
@ -1568,6 +1571,7 @@ class EmployeeViewSet(viewsets.ModelViewSet):
|
||||
"destroy",
|
||||
"add_skill",
|
||||
"remove_skill",
|
||||
"ldap_info", # Свой LDAP логин может видеть сам пользователь
|
||||
}:
|
||||
return [IsAuthenticated(), (IsSelfOrStaff | AdminOrActionOrModelPerms)()]
|
||||
|
||||
@ -2173,8 +2177,8 @@ class EmployeeViewSet(viewsets.ModelViewSet):
|
||||
# Сброс email_verified при изменении email
|
||||
if email_changed:
|
||||
logger.info("[ME PATCH] Step 13: Processing email change...")
|
||||
from django.utils.crypto import get_random_string
|
||||
from common.emails import send_templated_mail
|
||||
from django.utils.crypto import get_random_string
|
||||
|
||||
instance.email_verified = False
|
||||
instance.email_activation_code = get_random_string(6, "0123456789")
|
||||
@ -2260,68 +2264,70 @@ class EmployeeViewSet(viewsets.ModelViewSet):
|
||||
GET /api/v1/employees/{id}/ldap-info/
|
||||
|
||||
Получает LDAP информацию о сотруднике из Active Directory.
|
||||
|
||||
Доступ: только staff или пользователи с правом employees.view_ldap_info
|
||||
|
||||
|
||||
Доступ (через DRF permissions: IsSelfOrStaff | AdminOrActionOrModelPerms):
|
||||
- Сам пользователь может видеть свой LDAP логин
|
||||
- Staff/superuser могут видеть любые LDAP логины
|
||||
- Пользователи с правом employees.view_ldap_info могут видеть любые
|
||||
|
||||
Возвращает:
|
||||
- sAMAccountName: LDAP логин пользователя
|
||||
- userPrincipalName: полный UPN
|
||||
- displayName: отображаемое имя из LDAP
|
||||
|
||||
|
||||
Примечания:
|
||||
- Данные запрашиваются напрямую из LDAP, не из базы Django
|
||||
- Требуется активная связь с Active Directory
|
||||
- Проверка прав выполняется автоматически через get_permissions()
|
||||
"""
|
||||
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()
|
||||
|
||||
|
||||
emp = self.get_object() # DRF автоматически проверит права через get_permissions()
|
||||
|
||||
# Проверяем наличие 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
|
||||
status=status.HTTP_404_NOT_FOUND,
|
||||
)
|
||||
except LdapSyncState.DoesNotExist:
|
||||
return Response(
|
||||
{"detail": "У этого сотрудника нет связи с LDAP"},
|
||||
status=status.HTTP_404_NOT_FOUND
|
||||
status=status.HTTP_404_NOT_FOUND,
|
||||
)
|
||||
|
||||
|
||||
# Запрашиваем данные из LDAP
|
||||
try:
|
||||
ldap_repo = LdapRepository()
|
||||
attrs = ldap_repo.read_attrs(
|
||||
ldap_sync.ldap_dn,
|
||||
['sAMAccountName', 'userPrincipalName', 'displayName']
|
||||
["sAMAccountName", "userPrincipalName", "displayName"],
|
||||
)
|
||||
|
||||
if not attrs or not attrs.get('sAMAccountName'):
|
||||
|
||||
if not attrs or not attrs.get("sAMAccountName"):
|
||||
return Response(
|
||||
{"detail": "Не удалось получить LDAP информацию"},
|
||||
status=status.HTTP_404_NOT_FOUND
|
||||
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)
|
||||
|
||||
|
||||
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)
|
||||
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
|
||||
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
)
|
||||
|
||||
@action(detail=False, methods=["get"], url_path="export-excel")
|
||||
@ -2337,11 +2343,12 @@ class EmployeeViewSet(viewsets.ModelViewSet):
|
||||
Возвращает:
|
||||
- Excel файл (.xlsx) с данными сотрудников
|
||||
"""
|
||||
from openpyxl import Workbook
|
||||
from openpyxl.styles import Font, Alignment, PatternFill
|
||||
from django.http import HttpResponse
|
||||
from datetime import datetime
|
||||
|
||||
from django.http import HttpResponse
|
||||
from openpyxl import Workbook
|
||||
from openpyxl.styles import Alignment, Font, PatternFill
|
||||
|
||||
# Получаем отфильтрованный queryset
|
||||
queryset = self.filter_queryset(self.get_queryset())
|
||||
queryset = (
|
||||
@ -2413,7 +2420,7 @@ class EmployeeViewSet(viewsets.ModelViewSet):
|
||||
if not phone_field:
|
||||
return ""
|
||||
try:
|
||||
from phonenumbers import format_number, PhoneNumberFormat
|
||||
from phonenumbers import PhoneNumberFormat, format_number
|
||||
|
||||
return format_number(phone_field, PhoneNumberFormat.INTERNATIONAL)
|
||||
except Exception:
|
||||
@ -2705,8 +2712,8 @@ class EmployeeViewSet(viewsets.ModelViewSet):
|
||||
|
||||
# Сброс email_verified при изменении email
|
||||
if email_changed:
|
||||
from django.utils.crypto import get_random_string
|
||||
from common.emails import send_templated_mail
|
||||
from django.utils.crypto import get_random_string
|
||||
|
||||
emp.email_verified = False
|
||||
emp.email_activation_code = get_random_string(6, "0123456789")
|
||||
@ -3557,15 +3564,15 @@ class EmployeeActionViewSet(HistoryActionMixin, viewsets.ModelViewSet):
|
||||
# Если сотрудник не состоял ни в одном отделе, вручную перемещаем в OU=Dismissed
|
||||
if not departments_processed:
|
||||
try:
|
||||
from employees.ldap.utils.ldap_utils import (
|
||||
get_base_dn_for_employee,
|
||||
)
|
||||
from employees.ldap.infrastructure.connections import (
|
||||
_ldap,
|
||||
)
|
||||
from employees.ldap.repositories.ldap_repository import (
|
||||
ensure_container_exists,
|
||||
)
|
||||
from employees.ldap.utils.ldap_utils import (
|
||||
get_base_dn_for_employee,
|
||||
)
|
||||
from employees.models import LdapSyncState
|
||||
|
||||
sync_state = LdapSyncState.objects.filter(
|
||||
|
||||
@ -179,7 +179,7 @@ class Employee(AbstractUser):
|
||||
"Telegram Chat ID",
|
||||
max_length=100,
|
||||
blank=True,
|
||||
help_text="Ваш Telegram Chat ID для получения уведомлений (найдите через @userinfobot)"
|
||||
help_text="Ваш Telegram Chat ID для получения уведомлений (найдите через @userinfobot)",
|
||||
)
|
||||
whatsapp = PhoneNumberField("WhatsApp", blank=True)
|
||||
wechat = models.CharField("WeChat", max_length=100, blank=True)
|
||||
@ -516,10 +516,10 @@ class EmployeeDepartment(DateRangeMixin, models.Model):
|
||||
|
||||
class RoleAssignment(models.Model):
|
||||
"""Назначение роли сотруднику (не обязательно члену отдела).
|
||||
|
||||
|
||||
Позволяет назначать роли любым сотрудникам компании,
|
||||
независимо от их членства в отделе.
|
||||
|
||||
|
||||
Attributes:
|
||||
employee: Сотрудник, которому назначена роль.
|
||||
role: Назначенная роль отдела.
|
||||
@ -527,7 +527,7 @@ class RoleAssignment(models.Model):
|
||||
assigned_at: Дата/время назначения.
|
||||
is_active: Активно ли назначение.
|
||||
"""
|
||||
|
||||
|
||||
employee = models.ForeignKey(
|
||||
"Employee",
|
||||
on_delete=models.CASCADE,
|
||||
|
||||
@ -8,200 +8,202 @@
|
||||
*/
|
||||
|
||||
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();
|
||||
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;
|
||||
}
|
||||
|
||||
init() {
|
||||
if (!this.button || !this.textSpan || !this.icon) {
|
||||
console.error('LDAP Login Field: Required elements not found');
|
||||
return;
|
||||
}
|
||||
|
||||
this.button.addEventListener('click', () => this.toggleReveal());
|
||||
|
||||
this.button.addEventListener("click", () => this.toggleReveal());
|
||||
}
|
||||
|
||||
async toggleReveal() {
|
||||
if (this.isRevealed) {
|
||||
this.hide();
|
||||
} else {
|
||||
await this.reveal();
|
||||
}
|
||||
|
||||
async toggleReveal() {
|
||||
if (this.isRevealed) {
|
||||
this.hide();
|
||||
}
|
||||
|
||||
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 {
|
||||
await this.reveal();
|
||||
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");
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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 = `
|
||||
|
||||
// Убрать состояние ошибки через 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"
|
||||
<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;
|
||||
}
|
||||
|
||||
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);
|
||||
});
|
||||
const fields = document.querySelectorAll(".ldap-login-field");
|
||||
fields.forEach((field) => {
|
||||
new LdapLoginField(field);
|
||||
});
|
||||
}
|
||||
|
||||
// Инициализация при загрузке DOM
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', initLdapLoginFields);
|
||||
if (document.readyState === "loading") {
|
||||
document.addEventListener("DOMContentLoaded", initLdapLoginFields);
|
||||
} else {
|
||||
initLdapLoginFields();
|
||||
initLdapLoginFields();
|
||||
}
|
||||
|
||||
// Экспорт для использования в других модулях
|
||||
|
||||
@ -7,7 +7,7 @@
|
||||
.ldap-login-field {
|
||||
margin: var(--space-2) 0;
|
||||
text-align: center;
|
||||
|
||||
|
||||
.ldap-login-toggle {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
@ -21,32 +21,32 @@
|
||||
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;
|
||||
@ -55,19 +55,19 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Состояние загрузки
|
||||
&.loading {
|
||||
.ldap-login-toggle {
|
||||
pointer-events: none;
|
||||
opacity: 0.7;
|
||||
|
||||
|
||||
.ldap-icon {
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Состояние ошибки
|
||||
&.error {
|
||||
.ldap-login-toggle {
|
||||
@ -75,13 +75,13 @@
|
||||
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);
|
||||
}
|
||||
@ -105,7 +105,7 @@
|
||||
.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);
|
||||
|
||||
@ -45,7 +45,7 @@
|
||||
const editWrap = document.getElementById('editFormWrap');
|
||||
if (editWrap) {
|
||||
const isMobile = () => window.matchMedia('(max-width: 767.98px)').matches;
|
||||
|
||||
|
||||
function getHeaderOffset() {
|
||||
const sticky = document.querySelector('.ios-header');
|
||||
return (sticky?.offsetHeight || 0) + 8;
|
||||
@ -114,7 +114,7 @@
|
||||
// Отправка формы
|
||||
actionForm.addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
|
||||
// Скрываем предыдущие сообщения
|
||||
if (actionSuccess) actionSuccess.classList.add('d-none');
|
||||
if (actionError) actionError.classList.add('d-none');
|
||||
@ -144,7 +144,7 @@
|
||||
|
||||
if (response.ok) {
|
||||
const result = await response.json();
|
||||
|
||||
|
||||
// Показываем сообщение об успехе
|
||||
if (actionSuccess) {
|
||||
const actionNames = {
|
||||
@ -178,7 +178,7 @@
|
||||
} else {
|
||||
const error = await response.json();
|
||||
let errorMsg = 'Ошибка при создании события.';
|
||||
|
||||
|
||||
if (error.detail) {
|
||||
errorMsg = error.detail;
|
||||
} else if (error.error) {
|
||||
@ -186,7 +186,7 @@
|
||||
} else if (typeof error === 'object') {
|
||||
errorMsg = Object.values(error).flat().join('. ');
|
||||
}
|
||||
|
||||
|
||||
if (actionError) {
|
||||
actionError.textContent = errorMsg;
|
||||
actionError.classList.remove('d-none');
|
||||
|
||||
@ -66,8 +66,8 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{# LDAP Login (только для админов/HR) #}
|
||||
{% if request.user.is_staff or perms.employees.view_ldap_info %}
|
||||
{# LDAP Login (для себя или админов/HR) #}
|
||||
{% if is_me or 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 логин">
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user