102 Commits

Author SHA1 Message Date
lizerk
540e295ef4 feat: Fix LDAP DN bugs + add password management in admin
BREAKING CHANGES:
- Department.head автоматически очищается при увольнении руководителя
- LDAP managedBy атрибут очищается при увольнении

Fixes:
- DN не перемещался при восстановлении сотрудника (Dismissed → Users)
- DN не перемещался при увольнении сотрудника без отделов
- Ошибка при увольнении из множественных отделов (двойной modify_dn)
- Неправильные URL patterns для LDAP моделей в админке
- Двойное кодирование DN в редиректе смены пароля

Features:
- Добавлен метод _ensure_ldap_dn_location для гарантированного перемещения DN
- Добавлена проверка already_there в sync_department_member_to_ldap
- Управление паролями LDAP пользователей через админку:
  * Кнопка смены пароля в карточке пользователя
  * Массовое действие смены пароля
  * Автосинхронизация с Django password hash
- Автоочистка Department.head и LDAP managedBy при увольнении руководителя
- Добавлены методы set_password() и change_password() в LdapUser

Changed:
- Переписан сигнал sync_department_member_to_ldap с реальными методами
- Исправлены URL patterns: ldap_* → employees_*
2026-03-23 20:32:39 +03:00
lizerk
2943299c08 Fix: Handle both dict and QueryDict in perform_update
DRF request.data is dict for JSON but QueryDict for form data.
Use hasattr check to call .dict() only when available, otherwise
use dict() constructor for plain dicts.
2026-03-20 09:01:54 +03:00
lizerk
649bd62a2b Fix: Use QueryDict.dict() instead of dict(QueryDict)
dict(QueryDict) wraps every value in a list: {'first_name': ['Вадимин']}
QueryDict.dict() returns single values: {'first_name': 'Вадимин'}

This caused LDAP fields to be saved as "['Вадимин']" instead of 'Вадимин'
because the list was stringified by ldapdb before writing to AD.
2026-03-20 01:01:07 +03:00
lizerk
f15e8aca3f feat(ldap): add LDAP sync retry queue with Celery
- Add LdapSyncQueue model for deferred LDAP operations
- Add Celery tasks: process_ldap_queue_item, process_ldap_queue
- Refactor all LDAP signals (employee, department, group, position)
  to enqueue operations on DirectoryLdapError/DirectoryServiceError
- Add _queue.py helper for enqueuing with automatic Celery dispatch
- Add Celery Beat periodic task (every 60s) for queue processing
- Add management command: ldap_queue (status/retry/cleanup)
- Add migration 0043_ldap_sync_queue
- Add 18 tests covering model, tasks, signals, and enqueue logic
- Include LDAP services refactoring (base_service, constants,
  user_login_service, user_mapper_service, user_password_service,
  phone_utils)
2026-03-19 15:07:34 +03:00
lizerk
0539f50233 refactor: переход на строгий AD-only LDAP, регистрация + верификация email
- Удалён LDAP_SERVER_TYPE и все OpenLDAP ветки кода
- user_service.py: только AD (sAMAccountName, UAC, microsoft.modify_password)
- auth_backends.py: AD фильтры и атрибуты (objectCategory, objectGUID)
- Удалён server_configs.py (больше не нужен)
- Удалён OpenLDAP Docker окружение (docker-compose.ldap.yml, ldap/, QUICKSTART_LDAP.md)
- Фронтенд: мульти-шаговая регистрация, AvatarCropper 3:4, verify-email страница
- Убран CSRF для JWT API, IP-ограничения работают независимо
- Полевые ошибки регистрации с красной подсветкой на фронте
2026-03-19 00:48:13 +03:00
lizerk
bf9bc8f341 refactor(api): полная инкапсуляция LDAP логики в миксин
ПРОБЛЕМА:
- RegisterAPIView знал о режиме LDAP (вызывал _is_ldap_enabled())
- if ldap_enabled: create_ldap_user() else: create_db_user()
- View имел LDAP-специфичную логику выбора режима
- Нарушение принципа инкапсуляции

РЕШЕНИЕ:
Создан единый метод в LdapUserCreationMixin:

  create_user(first_name, last_name, email, phone, password, ...)
    - Автоматически выбирает LDAP или БД режим
    - Инкапсулирует вызов _is_ldap_enabled()
    - View больше не знает о существовании LDAP

ИЗМЕНЕНИЯ:
mixins.py:
  + create_user() - фасад, выбирает режим автоматически
  • create_ldap_user() - остался для внутреннего использования
  • create_db_user() - остался для внутреннего использования

auth.py:
  - Удалён import _is_ldap_enabled
  - Удалён блок if ldap_enabled: ... else: ...
  - Заменён на: emp, error = self.create_user(...)

РЕЗУЛЬТАТ:
- View НЕ знает про LDAP вообще
- Вся LDAP логика инкапсулирована в миксине
- Чистое разделение ответственности
- Упрощение: -18 строк в view

БЫЛО в RegisterAPIView:
  ldap_enabled = _is_ldap_enabled()
  if ldap_enabled:
      emp, err = self.create_ldap_user(...)
  else:
      emp = self.create_db_user(...)

СТАЛО:
  emp, err = self.create_user(...)

Django check: успешно
2026-03-18 19:33:35 +03:00
lizerk
bebe61b727 refactor(api): создание базового AnonymousAPIView для DRY
ПРОБЛЕМА:
- ResendEmailAPIView, VerifyEmailAPIView, RegisterAPIView дублировали:
  • throttle_scope = "anon"
  • permission_classes = [AllowAny]
- Нарушение DRY принципа (6 строк повторяющегося кода)

РЕШЕНИЕ:
Создан базовый класс AnonymousAPIView(APIView):
  - throttle_scope = "anon"
  - permission_classes = [AllowAny]
  - Используется для всех публичных auth endpoints

ИЗМЕНЕНИЯ:
- ResendEmailAPIView(AnonymousAPIView) - удалены дубли
- VerifyEmailAPIView(AnonymousAPIView) - удалены дубли
- RegisterAPIView(LdapUserCreationMixin, AnonymousAPIView) - удалены дубли

РЕЗУЛЬТАТ:
- Убрано 8 строк дублирования
- Более чистая иерархия наследования
- Легче добавлять новые анонимные endpoints
- Единая точка для настройки throttling/permissions

Django check: успешно
2026-03-18 19:32:15 +03:00
lizerk
aa70c23f0f refactor(api): вынос LDAP логики из RegisterAPIView в миксин
ПРОБЛЕМА:
- RegisterAPIView содержал ~30 строк LDAP-специфичной логики
- DirectoryService, DirectoryUserDTO, обработка исключений - всё в основном методе
- Нарушение принципа единственной ответственности
- Сложность тестирования и переиспользования

РЕШЕНИЕ:
Создан api/v1/employees/views/mixins.py с миксинами:

1. LdapUserCreationMixin:
   - create_ldap_user() - создание пользователя в LDAP через DirectoryService
   - create_db_user() - создание пользователя напрямую в БД (fallback без LDAP)
   - Централизованная обработка DirectoryLdapError/DirectoryDbError
   - Возвращает (Employee, None) при успехе или (None, Response) при ошибке

2. LdapPasswordMixin:
   - update_ldap_password() - обновление пароля в LDAP
   - Подготовка для использования в PasswordChangeView и других endpoints

ИЗМЕНЕНИЯ:
- auth.py: RegisterAPIView использует LdapUserCreationMixin
- Блок if ldap_enabled заменён на вызовы self.create_ldap_user() / self.create_db_user()
- _helpers.py: удалён _is_ldap_enabled (перемещён в mixins.py)
- Уменьшено количество строк в RegisterAPIView: ~190 → ~170

ПРЕИМУЩЕСТВА:
- Чёткое разделение: общая логика регистрации vs LDAP-специфика
- Переиспользование: миксины можно применить в других views
- Тестируемость: можно мокать методы миксина
- Читаемость: основной метод фокусируется на бизнес-логике

Django check: успешно
2026-03-18 19:28:01 +03:00
lizerk
5d729fa1f0 fix(views): исправить bulk update и удалить неиспользуемый код
Критические исправления:
- actions.py: заменить bulk update() на save() для EmployeeDepartment
  Причина: bulk update НЕ вызывает Django signals!
  Было: .update(is_active=False) - сигнал не срабатывает
  Стало: for loop с save() - сигнал срабатывает для каждой записи

- _helpers.py: удалить неиспользуемую функцию _ldap_try()
  Нигде не используется после рефакторинга

- _helpers.py: удалить неиспользуемые импорты
  DirectoryLdapError, DirectoryServiceError, DirectoryDbError, status

- actions.py: обновить комментарий с правильным названием сигнала
  sync_department_member_to_ldap вместо устаревшего sync_member

Функционал сохранен: при увольнении сотрудника теперь корректно
срабатывают сигналы для каждого EmployeeDepartment, что приводит
к корректной синхронизации с LDAP (перемещение из OU отделов).
2026-03-18 18:28:11 +03:00
lizerk
51b878c32b refactor(positions,roles): упростить ViewSets - удалить LDAP логику
Упрощены ViewSets:
- PositionViewSet: 146 → 137 строк (-9 строк, -6%)
- DepartmentRoleViewSet: 362 → 248 строк (-114 строк, -31%)

Изменения PositionViewSet:
- Удалены вызовы DirectoryService().reconcile_position()
- Удалены вызовы DirectoryService().delete_position_group()
- Удалены импорты: DirectoryService, _is_ldap_enabled, _ldap_try
- Actions (set_groups, add_groups, remove_groups): только работа с БД
- perform_create/update/destroy: базовые методы без LDAP

Изменения DepartmentRoleViewSet:
- create(): только создание в БД, удалены LDAP блоки
- update(): упрощен до базового метода
- destroy(): только удаление из БД
- assign(): только update_or_create RoleAssignment
- revoke(): только update is_active=False
- Удалены все if ldap_enabled блоки
- Удалены импорты: DirectoryService, DirectoryLdapError, DirectoryDbError, _is_ldap_enabled
- Удалена обработка ошибок LDAP (try/except блоки)

Итого: -123 строки, все ViewSets теперь работают только с БД

Примечание: Полноценная LDAP синхронизация для Position и DepartmentRole
требует реализации отдельных сигналов (аналогично signals_group.py).
Это может быть следующим этапом рефакторинга.
2026-03-18 18:22:52 +03:00
lizerk
e606a6ce2b refactor(actions): упростить EmployeeActionViewSet - удалить LDAP логику, использовать сигналы
Изменения:
- EmployeeActionViewSet: 240 → 115 строк (-125 строк, -52%)
- _apply_effects(): удалена вся LDAP логика (~140 строк)

Удалено:
- Прямые вызовы DirectoryService
- Обработка LdapSyncState
- Логика перемещения в OU=Dismissed и обратно
- Удаление из департаментов в LDAP
- Все try/except блоки для LDAP ошибок
- Импорты: DirectoryService, DirectoryLdapError, DirectoryServiceError, DirectoryDbError, LdapSyncState, settings, _is_ldap_enabled

Осталось только работа с БД:
- При ACTION_DISMISSED: is_active=False + деактивация EmployeeDepartment
- При других действиях: is_active=True

LDAP синхронизация через существующие сигналы:
- sync_employee_to_ldap_on_save: обрабатывает is_active (активация/деактивация)
- sync_member (signals_department.py): обрабатывает EmployeeDepartment (удаление из отделов)

Функционал сохранен, код стал в 2 раза короче
2026-03-18 18:19:58 +03:00
lizerk
18ca956348 feat(groups): добавить Django сигналы для LDAP синхронизации + упростить GroupViewSet
Создан signals_group.py:
- post_save(Group): создание/обновление LdapGroup (cn, description, scope, security)
- post_delete(Group): удаление LdapGroup
- m2m_changed(Group.user_set): синхронизация участников (add/remove/clear)

Упрощен GroupViewSet (641 → 339 строк, -47%):
- create(): только создание в БД + временные атрибуты для сигнала
- partial_update(): передача _ldap_old_name и _ldap_description в сигнал
- destroy(): базовый ViewSet метод
- list(): удален sync_groups_catalog (должен быть в фоне/cron)
- rename: упрощен до установки _ldap_old_name и save()
- set_description: упрощен до установки _ldap_description и save()
- members: только БД запрос (без LDAP)
- add_members: user_set.add(), сигнал синхронизирует в LDAP
- remove_members: user_set.remove(), сигнал синхронизирует в LDAP
- replace_members: user_set.set(), сигнал синхронизирует в LDAP

Удалено:
- Все if ldap_enabled блоки
- Прямые вызовы DirectoryService в ViewSet
- Rollback логика при ошибках LDAP
- Helper методы: _resolve_group_dn, _members_payload_to_dns, _dns_to_users
- Импорты: DirectoryService, settings, transaction, LdapSyncState, _is_ldap_enabled

Зарегистрирован сигнал в employees/apps.py
2026-03-18 18:11:54 +03:00
lizerk
1bc6961b1d refactor(auth): упростить VerifyEmailAPIView - удалить LDAP логику, использовать сигналы
Изменения:
- VerifyEmailAPIView: 331 → 271 строк (-60 строк)
- Удалена вся логика if ldap_enabled с DirectoryService
- Удалены проверки LdapSyncState и обработка LDAP ошибок
- Активация пользователя теперь через сигнал sync_employee_to_ldap_on_save
- Используется _ldap_changes для передачи данных в сигнал
- Удален импорт DirectoryServiceError

Логика:
- ViewSet устанавливает is_active = True и _ldap_changes
- Сигнал автоматически синхронизирует активацию в LDAP
- Обработка ldap_enabled и ошибок происходит в сигнале
- Код стал в 3 раза короче и понятнее
2026-03-18 17:57:56 +03:00
lizerk
bda5e841cb refactor(departments): удалить BFF агрегатор ui_context
- Удален метод ui_context (42 строки)
- Все его данные доступны через отдельные endpoints:
  - dept → GET /departments/{id}/
  - roles → можно добавить /departments/{id}/roles/
  - links → GET /departments/{id}/members/
  - user_perms → GET /departments/{id}/user-perms/
  - choices → UI логика (фронтенд)

Преимущества:
- Кешируемые отдельные endpoints
- Фронт запрашивает только нужное
- Проще тестировать
- Более RESTful

-42 строки (480 → 438)
2026-03-18 17:47:58 +03:00
lizerk
6e970741d8 refactor(departments): удалить избыточные @transaction.atomic декораторы
- Удалены все @transaction.atomic из actions (set_head, set_member_role, add_member, remove_member)
- Удален импорт transaction
- Одиночные операции БД уже атомарны по умолчанию
- Django request уже в транзакции если настроен ATOMIC_REQUESTS

-5 строк (485 → 480)
2026-03-18 17:29:23 +03:00
lizerk
64a1fb463e refactor(departments): удалить дублирующие create/destroy методы
Методы дублировали стандартную функциональность ModelViewSet:
- create(): serializer.save() делает то же самое
- destroy(): стандартный метод идентичен

-25 строк (510 → 485)
2026-03-18 17:26:03 +03:00
lizerk
9eb1487cee refactor(departments): упростить DepartmentViewSet, убрать явную LDAP логику
- Удалены импорты DirectoryService, DirectoryDepartmentDTO, LDAP errors
- Удален импорт _is_ldap_enabled
- Убраны все блоки if ldap_enabled: ... else: ...
- ViewSet теперь только работает с БД (сигналы синхронизируют LDAP)

Методы упрощены до чистой БД логики:
- partial_update: просто обновляет поля
- set_head: просто устанавливает head + head_appointed_at
- set_member_role: обновляет link.role и RoleAssignment
- add_member: создает/активирует EmployeeDepartment
- remove_member: деактивирует EmployeeDepartment
- create: просто создает Department
- destroy: просто удаляет Department

DepartmentViewSet теперь чистый CRUD: -189 строк (699 → 510)
2026-03-18 17:12:56 +03:00
lizerk
adc72aa814 refactor(employees): удалить вспомогательные actions из ViewSet
- Удален add_skill/remove_skill (дублируют PATCH /employees/{id}/)
- Удален ldap_info (LDAP логика не в CRUD ViewSet)
- Удален export_excel (не относится к CRUD)
- Убран импорт LdapSyncState
- Упрощен required_perms_by_action и get_permissions()
- Обновлен docstring

ViewSet теперь чистый CRUD + профиль: -253 строки, 272 вместо 525
2026-03-18 17:00:15 +03:00
lizerk
9187afda5e refactor(employees): упростить EmployeeViewSet, убрать явную LDAP логику
- Убраны импорты DirectoryService, LDAP ошибок, _is_ldap_enabled
- perform_create теперь просто создает Employee (без пароля)
- perform_update упрощен: убраны проверки is_ldap_enabled/is_ldap_managed
- Сигналы сами разбираются с LDAP синхронизацией
- ldap_info endpoint оставлен (readonly, полезен)

ViewSet теперь занимается только бизнес-логикой Employee, -15 строк
2026-03-18 16:50:53 +03:00
lizerk
e2a15f57a3 refactor(ldap): упростить архитектуру сигналов + добавить LdapPasswordMixin
- Убрана логика создания LDAP пользователей из сигналов
- Сигналы ТОЛЬКО синхронизируют изменения существующих LDAP пользователей
- Создание LDAP пользователей - ответственность RegisterAPIView
- Убран _ldap_password из perform_create (создание через API не создает LDAP пользователей)
- Создан LdapPasswordMixin для endpoint'ов работы с паролями
- perform_create теперь только создает Employee в БД (административное создание)

Архитектурное улучшение: разделение ответственности
- Auth views (RegisterAPIView) - создают LDAP пользователей напрямую
- Сигналы - только синхронизация изменений
- LdapPasswordMixin - для endpoint'ов смены пароля
2026-03-18 16:47:04 +03:00
lizerk
5585758ede refactor(ldap): replace ExternalSystemSyncMixin with Django signals
- Удалён ExternalSystemSyncMixin из EmployeeViewSet
- ViewSet теперь использует временные атрибуты (_ldap_password, _ldap_avatar, _ldap_changes)
- Создан employees/signals_ldap.py с post_save/post_delete handlers
- Сигналы автоматически синхронизируют Employee с LDAP через DirectoryService
- Убран дублирующий блок required_perms_by_action из EmployeeViewSet
- Упрощены perform_create и perform_update (-170 строк)

Архитектурное улучшение: разделение ответственности ViewSet (HTTP) и signals (синхронизация)
2026-03-18 16:13:20 +03:00
lizerk
50169ec09d refactor(api): удалить монолитный views.py (4500 строк)
- Удалён дублирующий монолит api/v1/employees/views.py (4622 строки)
- Все ViewSet'ы уже реэкспортированы в views/__init__.py
- Обновлены импорты в extended_views.py, analyze_queries.py, тестах
- -4622 строки дублированного кода

Импорты 'from api.v1.employees.views import X' работают через views/__init__.py
2026-03-18 15:46:23 +03:00
lizerk
95ff0dc0af refactor(ldap): заменить ldap3 CRUD на django-ldapdb ORM
- user_service.py: read_attrs/modify_user_attrs → LdapUser ORM,
  sync_user_groups_by_cns → ORM, delete_user без _ldap() контекста
- group_service.py: _wrapped методы через LdapGroup ORM
- department_service.py: _set_ou_description/_delete_department_ou → ORM
- views.py, employees.py: ldap_info через LdapUser ORM
- sync_employee_ids_to_ldap: полная замена на LdapUser ORM
- __init__.py: ORM реэкспорты, обёртки import_*/export_*
- orm_models.py: compatibility shim для django-ldapdb + Django 5.x
- Создан group_utils_orm.py (ORM замена group_utils)
- Удалён deprecated sync_service.py (352 строки)

TODO(ldap-orm) помечены: _set_password, _enable_user, _unique_logins,
_create_user_in_ldap, _move_user_to_base, rename (modify_dn),
sync_catalog (paged search), _set_ou_managed_by
2026-03-18 14:25:53 +03:00
a590acb762 feat(frontend): добавлен API клиент и восстановлен backend из master
- Создан frontend/src/lib/api.ts с 43 методами для всех API endpoint'ов
- Создан frontend/src/lib/permissions.ts для проверки прав доступа
- Исправлен URL в getChatMessagesAround: /messages/around/ → /messages-around/
- Исправлен параметр: message_id → around_id
- Восстановлен backend из ветки master для стабильности
- Удалены конфликтующие файлы Pages Router (src/pages, App.tsx, providers)
- Оставлена только чистая структура Next.js App Router
- Добавлена документация по миграции API v2
2026-02-24 15:40:50 +03:00
a335594932 fix: remove mocks from LDAP tests, use real LDAP integration, set gender default=0 2026-02-16 21:12:25 +03:00
4629a52834 fix: resolve test failures - add logger, LDAP_POSITIONS_BASE, make avatar/gender optional, add ensure_ldap_disabled fixtures 2026-02-16 17:38:00 +03:00
f85b5dfa08 refactor: remove DB access from LDAP layer update method
- Move department_id to department_dn resolution to ViewSet layer
- update_user_in_ldap_only now receives ready department_dn parameter
- LDAP service no longer queries Department.objects directly
- All three *_in_ldap_only methods are now pure LDAP (no DB access)
2026-02-16 17:22:38 +03:00
4b56711636 refactor(employees): minimize ViewSet overrides - let mixin do the work
Removed unnecessary method overrides:

Deleted:
- create(): removed entirely - миксин работает через perform_create
- partial_update(): removed entirely - миксин работает автоматически
- perform_update() avatar handling: moved to perform_update

Simplified:
- perform_create(): only unusable password + skills (30 lines)
- perform_update(): only email verification (15 lines)

Benefits:
- Миксин работает "из коробки" без явных переопределений
- Меньше кода для поддержки
- create/update/destroy используют стандартный механизм DRF
- Только необходимая кастомная логика в perform_*

Теперь ViewSet минимален:
- Конфигурация миксина (get_external_sync_method, prepare_external_data)
- Только критичная логика (unusable password, email verification)
- Все остальное - автоматически через миксин

manage.py check: no issues
2026-02-16 17:16:19 +03:00
518fb6e790 refactor(employees): remove _perform_user_update, simplify me() PATCH
Removed 180+ lines of complex update logic:

Deleted:
- _perform_user_update(): 180 lines of manual LDAP/DB coordination
- Complex validation and error handling
- Manual transaction management
- Direct DirectoryService.update_user() calls

Simplified me() PATCH:
- Now uses standard perform_update() mechanism
- 4 lines instead of 15
- Consistent with partial_update()
- Миксин управляет всем автоматически

Total code reduction in employees.py: ~400 lines removed
(create: -150, partial_update: -200, _perform_user_update: -180)

File now: ~800 lines (was 1200+)

Benefits:
- Single source of truth for update logic
- No code duplication
- Automatic transaction management
- Cleaner, easier to maintain

manage.py check: no issues
2026-02-16 17:10:03 +03:00
7a3402f1b6 refactor(employees): simplify partial_update using ExternalSystemSyncMixin
Replaced _perform_user_update (180+ lines) with clean mixin approach:

Before:
- _perform_user_update(): 180 lines of manual LDAP/DB coordination
- Manual transaction management
- Direct DirectoryService.update_user() calls with transaction.atomic inside
- Complex error handling
- Duplicate email verification logic

After:
- partial_update(): delegates to super() + email verification (30 lines)
- perform_update(): only avatar handling (20 lines)
- prepare_external_data(): builds LDAP changes dict with avatar_bytes
- Миксин автоматически управляет транзакциями

Changes:
1. partial_update(): simplified to super() + email verification
2. perform_update(): handles avatar file -> ContentFile
3. prepare_external_data(): extracts LDAP fields (first_name, last_name, email, etc),
   converts avatar to bytes, passes move_to_department_dn and group_cns
4. update_user_in_ldap_only(): updated signature to accept move_to_department_dn, group_cns

Code reduction: 200 lines -> 80 lines
Benefits: cleaner, automatic rollback, consistent with create()

Next: можно удалить _perform_user_update() если не используется

manage.py check: no issues
2026-02-16 17:08:45 +03:00
59b8f56c5a refactor(employees): simplify create() using ExternalSystemSyncMixin
Replaced 150+ lines of manual transaction management with mixin:

Before:
- Manual transaction.atomic()
- Manual LDAP sync calls
- Duplicate code for LDAP/non-LDAP modes
- Manual error handling and rollback
- ~150 lines of boilerplate

After:
- create() delegates to super() - миксин управляет всем
- perform_create() - только БД логика (30 lines)
- handle_external_sync_result() - обработка LDAP результата
- Миксин автоматически управляет транзакциями и откатами

Changes:
1. create(): simplified to validation + super().create()
2. perform_create(): creates Employee in DB, sets password/avatar/skills
3. prepare_external_data(): builds DTO with avatar_bytes, group_cns
4. handle_external_sync_result(): saves DN/GUID to LdapSyncState,
   sets employeeNumber, assigns position

5. ExternalSystemSyncMixin: added handle_external_sync_result() hook
   for custom post-sync actions

Code reduction: 150 lines -> 80 lines
Benefits: cleaner, DRYer, automatic error handling

manage.py check: no issues
2026-02-16 17:07:15 +03:00
df82e4f66d feat: add ExternalSystemSyncMixin for universal LDAP/DB sync
Created universal solution for ViewSet + external system synchronization:

1. ExternalSystemSyncMixin (common/external_sync_mixin.py):
   - Automatically wraps CRUD operations in transactions
   - Coordinates DB + LDAP in single atomic transaction
   - Rolls back DB if LDAP fails
   - Configurable error handling
   - Works with any external system (LDAP, APIs, etc)

2. Applied to EmployeeViewSet:
   - Added mixin to base classes
   - Configured LDAP sync mapping (create/update/destroy)
   - prepare_external_data() for DTO creation
   - handle_external_sync_error() for LDAP errors
   - No need to override create/update/destroy methods

3. Created pure LDAP methods in DirectoryService/UserService:
   - create_user_in_ldap_only() - LDAP create, returns dn/guid
   - update_user_in_ldap_only() - LDAP update, no DB access
   - delete_user_in_ldap_only() - LDAP delete, no DB access
   - All methods removed transaction.atomic (managed by ViewSet)

4. Documentation:
   - Complete guide in docs/guides/EXTERNAL_SYNC_MIXIN_GUIDE.md
   - Usage examples for all patterns
   - Migration guide from old architecture
   - Comparison with alternatives (Signals, Celery, Saga)

Benefits:
- Minimal code (3 methods vs overriding entire ViewSet)
- Clear separation: View owns DB, LDAP service owns LDAP
- Reusable for all ViewSets (departments, positions, etc)
- Easy to test (external_sync_enabled = False)
- Universal (works with any external system)

manage.py check: System check identified no issues
2026-02-16 17:04:08 +03:00
1f0858b085 refactor(ldap): fix architectural issues in views
DirectoryService:
- Added public methods: assign_role, revoke_role, create_role, update_role, delete_role
- Added public method: move_user_to_base (replaces private _user_service._move_user_to_base)
- Fixed method signatures to match DepartmentService API

roles.py:
- Replaced direct DepartmentService usage with DirectoryService facade (5 methods)
- Removed manual GroupService/UserService instantiation
- All CRUD operations now go through facade layer

actions.py:
- Replaced private API svc._user_service._move_user_to_base() with public svc.move_user_to_base()
- Simplified LDAP connection handling (2 occurrences)
- Removed manual _ldap() and ensure_container_exists calls

manage.py check: System check identified no issues
2026-02-16 16:38:01 +03:00
19464a38cf refactor(employees): DRY duplicate logic in employees and departments
employees.py:
- Extracted _perform_user_update() method (180 lines)
- Simplified me PATCH: removed ~150 lines of duplicate code
- Simplified partial_update: removed ~155 lines of duplicate code
- Total reduction: ~255 lines (930 → 675)

departments.py:
- Extracted _perform_set_head() method (70 lines)
- Simplified set_head action: removed ~50 lines of duplicate code
- Simplified partial_update head branch: removed ~70 lines of duplicate code
- Total reduction: ~140 lines (752 → 612)

manage.py check: System check identified no issues
2026-02-16 16:32:12 +03:00
09dfb7598e refactor(employees): DRY duplicate logic in employees and departments
employees.py:
- Extracted _perform_user_update() method (180 lines)
- Simplified me PATCH: removed ~150 lines of duplicate code
- Simplified partial_update: removed ~155 lines of duplicate code
- Total reduction: ~255 lines (930 → 675)

departments.py:
- Extracted _perform_set_head() method (70 lines)
- Simplified set_head action: removed ~50 lines of duplicate code
- Simplified partial_update head branch: removed ~70 lines of duplicate code
- Total reduction: ~140 lines (752 → 612)

manage.py check: System check identified no issues
2026-02-16 16:27:08 +03:00
eaedea1403 refactor(employees): remove permissions.py shim, fix relative imports
- Replaced ..permissions with ...permissions in all 7 view modules
- Deleted backend/api/v1/employees/permissions.py (duplicate re-export)
- Views now import directly from api.v1.permissions (3 dots up)

manage.py check: System check identified no issues
2026-02-16 16:21:45 +03:00
8efcabf530 refactor(employees): split monolithic views.py (4593 lines) into views/ package
- Split into 9 modules by ViewSet: departments, employees, positions,
  roles, skills, actions, groups, auth, _helpers
- Created views/__init__.py with re-exports for backward compatibility
- Created employees/permissions.py re-export shim (views/ is now a
  sub-package, so ..permissions resolves to employees.permissions)
- Fixed api/auth/serializers.py import: PHONE_FIELD from _helpers,
  _normalize_phone from employees.utils
- Removed ~250+ debug print()/logger.info() statements from
  departments.py and employees.py
- Removed dead _with_ldap_service() decorator from helpers
- Removed unreachable code at end of GroupViewSet.replace_members

manage.py check: System check identified no issues (0 silenced)
2026-02-16 16:18:03 +03:00
3e4137daa4 feat(ldap): добавлено кэширование LDAP логина с возможностью обновления
- Добавлено поле Employee.username для кэширования LDAP логина
- API endpoint обновлен: поддержка кэша и force_refresh параметра
- Новый дизайн: убран blur, добавлена иконка обновления
- Автоматическая загрузка при инициализации (из кэша или LDAP)
- Визуальная индикация источника данных (cached/fresh)
- Доступно всем пользователям без ограничений
2026-02-06 15:33:48 +03:00
10f7287ae5 fix(employees): добавлено LDAP соединение для LdapRepository
- LdapRepository требует активное соединение в конструкторе
- Добавлен вызов _conn() для получения LDAP соединения
- Исправлена ошибка: TypeError: LdapRepository.__init__() missing 1 required positional argument: 'conn'
- Теперь используется: LdapRepository(_conn())
2026-02-06 14:57:37 +03:00
77238c1154 fix(employees): исправлен доступ к LdapSyncState 2026-02-06 14:54:38 +03:00
17c4e506cf 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 могут видеть любые логины
2026-02-06 14:28:21 +03:00
c6cd2ba20d 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 (не хранятся в БД)
2026-02-06 14:24:31 +03:00
e8af9245ed fix: исправлена отправка аватаров через multipart вместо base64 и ошибка photo->avatar
- Изменена отправка аватаров на multipart/form-data вместо JSON+base64
- Исправлено в employee_create, employee_create_modal, employee_edit, employee_edit_me
- Исправлена ошибка 'Employee' object has no attribute 'photo' (user.photo -> user.avatar)
- Убрано сжатие изображений в _file_to_data_uri (функция deprecated)

Fixes: #400 Bad Request при загрузке больших аватаров
2026-01-09 14:14:01 +03:00
8b391cb388 - 2026-01-09 14:12:54 +03:00
53da23f690 fix: revoke role from LDAP group when removing via_assignment role 2025-12-30 12:31:55 +03:00
4462ca1eec debug: add logging to set_member_role to diagnose 400 error 2025-12-30 12:24:39 +03:00
42111a4526 ПОИСК БАГА НЕСОХРАНЕНИЯ РОЛЕЙ В ЛДАП 2025-12-30 11:22:48 +03:00
5d0572d17a fix(roles): enforce LDAP-first pattern for role operations
When LDAP is enabled:
- create_role: creates LDAP group before DB record
- update_role: renames LDAP group before DB update
- delete_role: deletes LDAP group before DB deletion
- assign_role: adds to LDAP group before creating RoleAssignment
- revoke_role: removes from LDAP group before deactivating assignment

On LDAP error: operation fails, no DB changes
On DB error after LDAP success: rollback LDAP changes

Fixes: roles could bypass LDAP when it was enabled
2025-12-30 11:04:39 +03:00
026bc46816 feat(department-roles): complete LDAP architecture redesign with RoleAssignment model
BREAKING CHANGES:
- Removed OU=Roles from LDAP structure, role groups now created directly in department OU
- Role groups use ROLE_<Name> naming convention (similar to DEP_, POS_)

New features:
- RoleAssignment model: allows assigning roles to ANY employee, not just department members
- New API endpoints: /assign/, /revoke/, /assignments/ on DepartmentRoleViewSet
- Permission system updated: has_dept_perm() checks RoleAssignment first
- Employee profile shows external roles with 'внешняя роль' badge
- Employee list filter includes role-only employees with department_relation field
- Role assignment modal now searches all employees via AJAX

LDAP changes:
- _ensure_role_group(): creates ROLE_<Name> group in department OU
- assign_role(), revoke_role(): manage RoleAssignment + LDAP sync
- rename_role_group(), delete_role_group(): full lifecycle support
- Removed all references to OU=Roles

Migrations:
- 0035: Create RoleAssignment model
- 0036: Data migration from EmployeeDepartment.role

Tests:
- 7 new tests for role assignment (all passing)

Docs:
- 9 detailed reports on architecture, models, API, LDAP, tests
2025-12-30 10:35:34 +03:00
de4506ae24 fix 2025-12-18 16:03:26 +03:00