Skip to content

Commit 458a6d3

Browse files
authored
Merge pull request #20 from mikopbx/develop
refactor: migrate to REST API v3 for employee management
2 parents 54a4d85 + c6394c9 commit 458a6d3

30 files changed

+559
-242
lines changed

App/Controllers/ModuleLdapSyncController.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ public function indexAction(): void
6969
*
7070
* @return void
7171
*/
72-
public function modifyAction(string $id = null): void
72+
public function modifyAction(?string $id = null): void
7373
{
7474
$this->view->showModuleStatusToggle = false;
7575
$footerCollection = $this->assets->collection(AssetProvider::FOOTER_JS);

CLAUDE.md

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
# CLAUDE.md
2+
3+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4+
5+
## Project Overview
6+
7+
ModuleLdapSync is a MikoPBX extension module that synchronizes employees from LDAP/Active Directory servers into MikoPBX. It creates user accounts automatically and supports bidirectional sync - extension details can be synchronized back to the domain.
8+
9+
## Build & Development Commands
10+
11+
### PHP Syntax Check
12+
```bash
13+
php -l <file.php>
14+
```
15+
16+
### JavaScript Compilation
17+
```bash
18+
/Users/nb/PhpstormProjects/mikopbx/MikoPBXUtils/node_modules/.bin/babel "public/assets/js/src/<file>.js" --out-dir "public/assets/js/" --source-maps inline --presets airbnb
19+
```
20+
21+
### Code Quality
22+
```bash
23+
phpstan analyse <file.php>
24+
```
25+
26+
## Architecture
27+
28+
### Directory Structure
29+
- `App/` - Phalcon MVC components (Controllers, Forms)
30+
- `Lib/` - Core business logic (LDAP sync, connectors, workers)
31+
- `Models/` - Database models (Phalcon ORM)
32+
- `Messages/` - i18n translation files
33+
- `public/assets/` - Frontend assets (JS source in `js/src/`, compiled in `js/`)
34+
- `Setup/` - Module installation/uninstallation logic
35+
36+
### Key Components
37+
38+
**LdapSyncMain** (`Lib/LdapSyncMain.php`)
39+
- Main orchestrator for LDAP synchronization
40+
- `syncAllUsers()` - syncs all enabled LDAP servers
41+
- `syncUsersPerServer()` - syncs single server, returns AnswerStructure
42+
- `updateUserData()` - bidirectional sync logic using hash comparison
43+
- Uses MikoPBX REST API for user CRUD operations
44+
45+
**LdapSyncConnector** (`Lib/LdapSyncConnector.php`)
46+
- LDAP connection management using `ldaprecord/ldaprecord` library
47+
- Supports ActiveDirectory, OpenLDAP, DirectoryServer, FreeIPA
48+
- Handles user queries with pagination via Redis cache
49+
- `getUsersList()` - fetches users from LDAP with attribute mapping
50+
- `updateDomainUser()` - writes changes back to LDAP
51+
52+
**WorkerLdapSync** (`Lib/Workers/WorkerLdapSync.php`)
53+
- Background worker for periodic sync (hourly by default)
54+
- Uses Redis cache for sync frequency management
55+
- Increases frequency to 5 minutes when changes detected
56+
57+
**Constants** (`Lib/Constants.php`)
58+
- User attribute mappings (name, mobile, extension, email, avatar, password)
59+
- Sync result states (UPDATED, SKIPPED, CONFLICT)
60+
- Conflict tracking constants
61+
62+
### Data Flow
63+
1. Worker triggers `LdapSyncMain::syncAllUsers()`
64+
2. For each enabled server, `LdapSyncConnector` fetches LDAP users
65+
3. Hash comparison determines which side changed (domain vs PBX)
66+
4. Updates applied via MikoPBX REST API or `LdapSyncConnector::updateDomainUser()`
67+
5. Conflicts recorded in `Conflicts` model
68+
69+
### Models
70+
- `LdapServers` - LDAP server configurations (host, port, credentials, attribute mappings)
71+
- `ADUsers` - Tracks synced users with domain/local hashes for change detection
72+
- `Conflicts` - Records sync conflicts for manual resolution
73+
74+
### REST API Endpoints (via ModuleLdapSyncController + ApiController)
75+
- `get-available-ldap-users` - Test LDAP connection and fetch users
76+
- `sync-ldap-users` - Trigger manual sync
77+
- `get-server-conflicts` / `delete-server-conflict(s)` - Conflict management
78+
- `get-disabled-ldap-users` - List disabled domain users in PBX
79+
80+
### Frontend
81+
- `module-ldap-sync-modify.js` - Main config form (server settings, attribute mapping, manual sync)
82+
- `module-ldap-sync-index.js` - Server list management
83+
- Uses Semantic UI components and MikoPBX Form/PbxApi utilities
84+
85+
## CI/CD
86+
87+
GitHub Actions workflow (`.github/workflows/build.yml`) uses shared MikoPBX workflow for building and publishing to MikoPBX marketplace.

Lib/LdapSyncMain.php

Lines changed: 96 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@
2424
use MikoPBX\Common\Models\Users;
2525
use MikoPBX\Common\Providers\PBXCoreRESTClientProvider;
2626
use MikoPBX\Modules\Logger;
27-
use MikoPBX\PBXCoreREST\Lib\Extensions\DataStructure;
2827
use Modules\ModuleLdapSync\Lib\Workers\WorkerLdapSync;
2928
use Modules\ModuleLdapSync\Models\ADUsers;
3029
use Modules\ModuleLdapSync\Models\LdapServers;
@@ -355,13 +354,13 @@ public static function getUserOnMikoPBX(string $userId): array
355354
}
356355

357356
/**
358-
* Creates or updates a user using provided data.
357+
* Creates or updates a user using provided data via REST API v3.
359358
*
360359
* @param array $userDataFromLdap - User data to be created or updated.
361360
* @param ?string $currentUserId The current user id.
362361
* @return AnswerStructure
363362
*/
364-
public static function createUpdateUser(array $userDataFromLdap, string $currentUserId = null): AnswerStructure
363+
public static function createUpdateUser(array $userDataFromLdap, ?string $currentUserId = null): AnswerStructure
365364
{
366365
$pbxUserData = self::findUserInMikoPBX($userDataFromLdap, $currentUserId);
367366

@@ -374,91 +373,134 @@ public static function createUpdateUser(array $userDataFromLdap, string $current
374373
return $result;
375374
}
376375

377-
// Get user data from the API
378-
$di=MikoPBXVersion::getDefaultDi();
379-
$restAnswer = $di->get(PBXCoreRESTClientProvider::SERVICE_NAME, [
380-
'/pbxcore/api/extensions/getRecord',
381-
PBXCoreRESTClientProvider::HTTP_METHOD_GET,
382-
['id' => $pbxUserData['extension_id'] ?? '']
383-
]);
376+
$di = MikoPBXVersion::getDefaultDi();
377+
$isNewEmployee = empty($pbxUserData['user_id']);
378+
379+
// Get existing employee data or defaults for new employee
380+
if ($isNewEmployee) {
381+
// Get default template for new employee
382+
$restAnswer = $di->get(PBXCoreRESTClientProvider::SERVICE_NAME, [
383+
'/pbxcore/api/v3/employees:getDefault',
384+
PBXCoreRESTClientProvider::HTTP_METHOD_GET
385+
]);
386+
} else {
387+
// Get existing employee data by user_id
388+
$restAnswer = $di->get(PBXCoreRESTClientProvider::SERVICE_NAME, [
389+
'/pbxcore/api/v3/employees/' . $pbxUserData['user_id'],
390+
PBXCoreRESTClientProvider::HTTP_METHOD_GET
391+
]);
392+
}
393+
384394
if (!$restAnswer->success) {
385395
return new AnswerStructure($restAnswer);
386396
}
387397

388-
// Create a new data structure for user data
389-
$dataStructure = new DataStructure($restAnswer->data);
398+
$employeeData = $restAnswer->data;
390399

391400
// Check if provided phone number is available
392-
$number = $userDataFromLdap[Constants::USER_EXTENSION_ATTR];
393-
if (!empty($number) && $number !== $dataStructure->number) {
401+
$number = $userDataFromLdap[Constants::USER_EXTENSION_ATTR] ?? null;
402+
if (!empty($number) && $number !== ($employeeData['number'] ?? '')) {
394403
$restAnswer = $di->get(PBXCoreRESTClientProvider::SERVICE_NAME, [
395-
'/pbxcore/api/extensions/available',
396-
PBXCoreRESTClientProvider::HTTP_METHOD_GET,
397-
['number' => $number]
404+
'/pbxcore/api/v3/extensions:available',
405+
PBXCoreRESTClientProvider::HTTP_METHOD_POST,
406+
['number' => $number],
407+
['Content-Type' => 'application/json']
398408
]);
399-
if ($restAnswer->success || $restAnswer->data['userId'] == $pbxUserData['user_id']) {
400-
$dataStructure->number = $number;
409+
if ($restAnswer->success || ($restAnswer->data['userId'] ?? null) == $pbxUserData['user_id']) {
410+
$employeeData['number'] = $number;
401411
}
402412
}
403413

404414
// Check if provided mobile number is available
405-
$mobileFromDomain = $userDataFromLdap[Constants::USER_MOBILE_ATTR];
406-
if (!empty($mobileFromDomain) && $mobileFromDomain !== $dataStructure->mobile_number) {
415+
$mobileFromDomain = $userDataFromLdap[Constants::USER_MOBILE_ATTR] ?? null;
416+
if (!empty($mobileFromDomain) && $mobileFromDomain !== ($employeeData['mobile_number'] ?? '')) {
407417
$restAnswer = $di->get(PBXCoreRESTClientProvider::SERVICE_NAME, [
408-
'/pbxcore/api/extensions/available',
409-
PBXCoreRESTClientProvider::HTTP_METHOD_GET,
410-
['number' => $mobileFromDomain]
418+
'/pbxcore/api/v3/extensions:available',
419+
PBXCoreRESTClientProvider::HTTP_METHOD_POST,
420+
['number' => $mobileFromDomain],
421+
['Content-Type' => 'application/json']
411422
]);
412-
if ($restAnswer->success || $restAnswer->data['userId'] == $pbxUserData['user_id']) {
423+
if ($restAnswer->success || ($restAnswer->data['userId'] ?? null) == $pbxUserData['user_id']) {
413424
// Update mobile number and forwarding settings
414-
$oldMobileNumber = $dataStructure->mobile_number;
415-
$dataStructure->mobile_number = $mobileFromDomain;
416-
$dataStructure->mobile_dialstring = $mobileFromDomain;
425+
$oldMobileNumber = $employeeData['mobile_number'] ?? '';
426+
$employeeData['mobile_number'] = $mobileFromDomain;
427+
$employeeData['mobile_dialstring'] = $mobileFromDomain;
417428

418-
if ($oldMobileNumber === $dataStructure->fwd_forwardingonunavailable) {
419-
$dataStructure->fwd_forwardingonunavailable = $mobileFromDomain;
429+
if ($oldMobileNumber === ($employeeData['fwd_forwardingonunavailable'] ?? '')) {
430+
$employeeData['fwd_forwardingonunavailable'] = $mobileFromDomain;
420431
}
421-
if ($oldMobileNumber === $dataStructure->fwd_forwarding) {
422-
$dataStructure->fwd_forwarding = $mobileFromDomain;
432+
if ($oldMobileNumber === ($employeeData['fwd_forwarding'] ?? '')) {
433+
$employeeData['fwd_forwarding'] = $mobileFromDomain;
423434
}
424-
if ($oldMobileNumber === $dataStructure->fwd_forwardingonbusy) {
425-
$dataStructure->fwd_forwardingonbusy = $mobileFromDomain;
435+
if ($oldMobileNumber === ($employeeData['fwd_forwardingonbusy'] ?? '')) {
436+
$employeeData['fwd_forwardingonbusy'] = $mobileFromDomain;
426437
}
427438
}
428439
}
429440

430441
// Check if provided email is available
431-
$email = $userDataFromLdap[Constants::USER_EMAIL_ATTR]??null;
432-
if (!empty($email) && $email !== $dataStructure->user_email) {
442+
$email = $userDataFromLdap[Constants::USER_EMAIL_ATTR] ?? null;
443+
if (!empty($email) && $email !== ($employeeData['user_email'] ?? '')) {
433444
$restAnswer = $di->get(PBXCoreRESTClientProvider::SERVICE_NAME, [
434-
'/pbxcore/api/users/available',
445+
'/pbxcore/api/v3/users:available',
435446
PBXCoreRESTClientProvider::HTTP_METHOD_GET,
436447
['email' => $email]
437448
]);
438-
if ($restAnswer->success || $restAnswer->data['userId'] == $pbxUserData['user_id']) {
439-
$dataStructure->user_email = $email;
449+
if ($restAnswer->success || ($restAnswer->data['userId'] ?? null) == $pbxUserData['user_id']) {
450+
$employeeData['user_email'] = $email;
440451
}
441452
}
442453

443-
// Check provided sip password
444-
$sipPassword = $userDataFromLdap[Constants::USER_PASSWORD_ATTR] ?? $dataStructure->sip_secret;
445-
if (!empty($sipPassword) && $sipPassword != $dataStructure->sip_secret) {
446-
$dataStructure->sip_secret = $sipPassword;
454+
// Update SIP password if provided
455+
$sipPassword = $userDataFromLdap[Constants::USER_PASSWORD_ATTR] ?? null;
456+
if (!empty($sipPassword) && $sipPassword !== ($employeeData['sip_secret'] ?? '')) {
457+
$employeeData['sip_secret'] = $sipPassword;
447458
}
448459

449-
$dataStructure->user_username = $userDataFromLdap[Constants::USER_NAME_ATTR] ?? $dataStructure->user_username;
450-
451-
$dataStructure->user_avatar = $userDataFromLdap[Constants::USER_AVATAR_ATTR] ?? $dataStructure->user_avatar;
460+
// Update username
461+
if (!empty($userDataFromLdap[Constants::USER_NAME_ATTR])) {
462+
$employeeData['user_username'] = $userDataFromLdap[Constants::USER_NAME_ATTR];
463+
}
452464

453-
$dataStructure->sip_transport = trim($dataStructure->sip_transport);
465+
// Update avatar
466+
if (!empty($userDataFromLdap[Constants::USER_AVATAR_ATTR])) {
467+
$employeeData['user_avatar'] = $userDataFromLdap[Constants::USER_AVATAR_ATTR];
468+
}
454469

470+
// Trim transport value
471+
if (isset($employeeData['sip_transport'])) {
472+
$employeeData['sip_transport'] = trim($employeeData['sip_transport']);
473+
}
455474

456-
// Save user data through the CORE API
457-
$restAnswer = $di->get(PBXCoreRESTClientProvider::SERVICE_NAME, [
458-
'/pbxcore/api/extensions/saveRecord',
459-
PBXCoreRESTClientProvider::HTTP_METHOD_POST,
460-
$dataStructure->toArray()
461-
]);
475+
// Remove read-only fields before saving
476+
unset(
477+
$employeeData['extensions_length'],
478+
$employeeData['sip_networkfilterid_represent'],
479+
$employeeData['fwd_forwarding_represent'],
480+
$employeeData['fwd_forwardingonbusy_represent'],
481+
$employeeData['fwd_forwardingonunavailable_represent'],
482+
$employeeData['represent'],
483+
$employeeData['search_index']
484+
);
485+
486+
// Save employee data through the REST API v3
487+
if ($isNewEmployee) {
488+
// Create new employee
489+
$restAnswer = $di->get(PBXCoreRESTClientProvider::SERVICE_NAME, [
490+
'/pbxcore/api/v3/employees',
491+
PBXCoreRESTClientProvider::HTTP_METHOD_POST,
492+
$employeeData,
493+
['Content-Type' => 'application/json']
494+
]);
495+
} else {
496+
// Update existing employee using PATCH (partial update)
497+
$restAnswer = $di->get(PBXCoreRESTClientProvider::SERVICE_NAME, [
498+
'/pbxcore/api/v3/employees/' . $pbxUserData['user_id'],
499+
PBXCoreRESTClientProvider::HTTP_METHOD_PATCH,
500+
$employeeData,
501+
['Content-Type' => 'application/json']
502+
]);
503+
}
462504

463505
return new AnswerStructure($restAnswer);
464506
}
@@ -470,7 +512,7 @@ public static function createUpdateUser(array $userDataFromLdap, string $current
470512
* @param ?string $currentUserId The current user id.
471513
* @return array The user data if found, otherwise an empty array.
472514
*/
473-
public static function findUserInMikoPBX(array $userDataFromLdap, string $currentUserId = null): array
515+
public static function findUserInMikoPBX(array $userDataFromLdap, ?string $currentUserId = null): array
474516
{
475517
$parameters = [
476518
'models' => [

Messages/az.php

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -35,13 +35,13 @@
3535
'module_ldap_LdapBaseDN' => 'Domen kökü',
3636
'module_ldap_LdapPassword' => 'parol',
3737
'module_ldap_LdapAttributesHeader' => 'MikoPBX-də verilənlərlə uyğunluq üçün domendəki atributlar',
38-
'module_ldap_UserExtensionAttribute' => 'istifadəçi uzantı nömrəsi',
39-
'module_ldap_UserMobileAttribute' => 'mobil telefon',
38+
'module_ldap_UserExtensionAttribute' => 'İstifadəçinin daxili nömrəsi',
39+
'module_ldap_UserMobileAttribute' => 'Mobil telefon',
4040
'module_ldap_UserEmailAttribute' => 'E-poçt ünvanı',
41-
'module_ldap_UserNameAttribute' => 'istifadəçinin adı və soyadı',
42-
'module_ldap_UserAccountControl' => 'istifadəçinin kilid statusunun saxlandığı atribut',
43-
'module_ldap_UserAvatarAttribute' => 'foto atribut',
44-
'module_ldap_UpdateAttributes' => 'MikoPBX-də dəyişdikdə domendəki telefon nömrələrini yeniləyin (yazma icazələri tələb olunur)',
41+
'module_ldap_UserNameAttribute' => 'İstifadəçinin adı və soyadı',
42+
'module_ldap_UserAccountControl' => 'İstifadəçinin bloklama statusunun saxlandığı atribut',
43+
'module_ldap_UserAvatarAttribute' => 'Foto ilə atribut',
44+
'module_ldap_UpdateAttributes' => 'Domendəki məlumatları MikoPBX-də dəyişdirərkən yeniləyin (yazma hüquqları tələb olunur)',
4545
'module_ldap_LdapOrganizationalUnit' => 'Bölmə',
4646
'module_ldap_LdapUserFilter' => 'Əlavə istifadəçi filtri',
4747
'module_ldap_LdapCheckGetListHeader' => 'LDAP istifadəçilərinin siyahısını əldə etmək üçün test edin',
@@ -88,7 +88,7 @@
8888
'module_ldap_UserName' => 'İstifadəçi adı',
8989
'module_ldap_UserNumber' => 'Uzatma nömrəsi',
9090
'module_ldap_findExtension' => 'İstifadəçilər siyahısında tapın',
91-
'module_ldap_DeletedUsersHeader' => 'LDAP/AD-də uzaqdan işləyən işçilər',
92-
'module_ldap_DeletedUsersEmpty' => 'Uzaqdan işçilər yoxdur',
91+
'module_ldap_DeletedUsersHeader' => 'LDAP/AD-də əlil olan işçilər',
92+
'module_ldap_DeletedUsersEmpty' => 'Əlil işçi yoxdur',
9393
'module_ldap_UserEmail' => 'E-poçt',
9494
];

Messages/cs.php

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -35,13 +35,13 @@
3535
'module_ldap_LdapBaseDN' => 'Kořen domény',
3636
'module_ldap_LdapPassword' => 'Heslo',
3737
'module_ldap_LdapAttributesHeader' => 'Atributy v doméně pro párování s daty v MikoPBX',
38-
'module_ldap_UserExtensionAttribute' => 'číslo uživatelské pobočky',
39-
'module_ldap_UserMobileAttribute' => 'mobilní telefon',
40-
'module_ldap_UserEmailAttribute' => 'Emailová adresa',
41-
'module_ldap_UserNameAttribute' => 'jméno a příjmení uživatele',
42-
'module_ldap_UserAccountControl' => 'atribut, kde je uložen stav uzamčení uživatele',
43-
'module_ldap_UserAvatarAttribute' => 'atribut fotografie',
44-
'module_ldap_UpdateAttributes' => 'Aktualizace telefonních čísel v doméně, když se změní v MikoPBX (vyžaduje oprávnění k zápisu)',
38+
'module_ldap_UserExtensionAttribute' => 'Interní číslo uživatele',
39+
'module_ldap_UserMobileAttribute' => 'Mobilní telefon',
40+
'module_ldap_UserEmailAttribute' => 'E-mailová adresa',
41+
'module_ldap_UserNameAttribute' => 'Jméno a příjmení uživatele',
42+
'module_ldap_UserAccountControl' => 'Atribut, kde je uložen stav blokování uživatele',
43+
'module_ldap_UserAvatarAttribute' => 'Atribut s fotografií',
44+
'module_ldap_UpdateAttributes' => 'Aktualizace dat v doméně při její změně v MikoPBX (vyžaduje se oprávnění k zápisu)',
4545
'module_ldap_LdapOrganizationalUnit' => 'Pododdělení',
4646
'module_ldap_LdapUserFilter' => 'Další uživatelský filtr',
4747
'module_ldap_LdapCheckGetListHeader' => 'Otestujte a získejte seznam uživatelů LDAP',
@@ -88,7 +88,7 @@
8888
'module_ldap_UserName' => 'Uživatelské jméno',
8989
'module_ldap_UserNumber' => 'Číslo pobočky',
9090
'module_ldap_findExtension' => 'Najděte v seznamu uživatelů',
91-
'module_ldap_DeletedUsersHeader' => 'Zaměstnanci vzdálení v LDAP/AD',
92-
'module_ldap_DeletedUsersEmpty' => 'Žádní vzdálení zaměstnanci',
91+
'module_ldap_DeletedUsersHeader' => 'Zaměstnanci zakázaní v LDAP/AD',
92+
'module_ldap_DeletedUsersEmpty' => 'Žádní zaměstnanci se zdravotním postižením',
9393
'module_ldap_UserEmail' => 'E-mail',
9494
];

0 commit comments

Comments
 (0)