diff --git a/.env.example b/.env.example index 9ba2d8b..b2afb6b 100644 --- a/.env.example +++ b/.env.example @@ -63,3 +63,21 @@ AWS_BUCKET= AWS_USE_PATH_STYLE_ENDPOINT=false VITE_APP_NAME="${APP_NAME}" + +DOLIBARR_URL= +LIBARR_USERNAME= +DOLIBARR_PWD= + +MAIL_ISPAPI_URL= +MAIL_ISPAPI_USERNAME= +MAIL_ISPAPI_PWD= + +HOSTING_ISPAPI_URL= +HOSTING_ISPAPI_USERNAME= +HOSTING_ISPAPI_PWD= + +#NEXTCLOUD_API_ID= +#NEXCLOUD_API_PWD= + +#SYMPA_API_ID= +#SYMPA_API_PWD= diff --git a/README.md b/README.md index d24080b..9e523ab 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,16 @@ # LRL APP - Centralized Portal with Laravel, React & Keycloak SSO -This project is a **centralized portal application** built with **Laravel 12 & React19**, designed to provide a seamless and secure entry point for both **end users** and **administrators**. +This project is a **centralized portal application & web hosting association ERP** built with **Laravel 12 & React19**, designed to provide a seamless and secure entry point for both **end users** and **administrators**. - **Front Office (Users)** - Build on React19 - (V2) Authentication via **Keycloak SSO (OIDC)** - Unified dashboard to access external applications (cloud storage, mailing tools, file sharing, etc.) - Role-based access control synced from Keycloak + - Connected with : ISP Config for web hosting and mailbox management, NextCloud, Sympa for mailing list and more... - **Back Office (Admins)** - Authentication handled **locally in Laravel** (separate from Keycloak) - - Built with **FilamentPHP** for a modern and intuitive admin panel + - Built with **FilamentPHP** - Advanced admin features: app configuration, user activity logs, monitoring - **Security & API** - JWT validation for user-facing APIs (via Keycloak) @@ -20,10 +21,10 @@ This project is a **centralized portal application** built with **Laravel 12 & R - Blade + Livewire (back office UI) - React19 (front office UI) - TailwindCSS (UI framework) - - Keycloak SSO (OIDC) + - Keycloak SSO (OIDC) (V2) - FilamentPHP (admin panel) - Redis (cache, sessions, queues) - - MySQL (PostgreSQL coming soon) - - Docker-ready + CI/CD support (coming soon) + - Maria DB + - Docker-ready + CI/CD support and automated deploy This architecture allows associations to **centralize authentication and app access** while keeping the **admin back office independent and highly secure**. diff --git a/app/Console/Commands/SyncDolibarrMembers.php b/app/Console/Commands/SyncDolibarrMembers.php new file mode 100644 index 0000000..4627253 --- /dev/null +++ b/app/Console/Commands/SyncDolibarrMembers.php @@ -0,0 +1,86 @@ +info('Starting Dolibarr members import...'); + // Dolibarr API call + $client = new DolibarrService; + $doliMembers = collect($client->getAllMembers()); + + $progressBar = $this->output->createProgressBar(count($doliMembers)); + $progressBar->start(); + $i = 0; + + foreach ($doliMembers as $member) { + dd($member); + + $newMember = Member::updateOrCreate([ + 'dolibarr_id' => $member->id, + ], [ + 'status' => $member['status'], // @todo: faire concorder les statuts + 'nature' => 'physical', + 'member_type' => $member['type'], + 'group_id' => null, + 'lastname' => $member['firstname'], + 'firstname' => $member['lastname'], + 'email' => $member['email'], + 'personal_email' => '', + 'company' => '', + 'website_url' => $member['url'], + 'date_of_birth' => '', + 'address' => '', + 'zipcode' => '', + 'city' => '', + 'country' => '', + 'phone1' => '', + 'phone2' => '', + 'public_membership' => '', + 'created_at' => Carbon::create($member['date_creation'])->format(timestamp()), + 'updated_at' => '', + ]); + + // On crée l'adhérent en remplissant les données en bdd avec ses coordonnées, son statut etc ... + + // On récupère toutes les adhésions/cotisations pour chaque adhérent + $memberships = $client->getMemberSubscriptions($member->id); + + // on traite les notes (privée, publique, lien ect) contenues dans dolibarr + + $i++; + } + + $progressBar->finish(); + // Logs + + $this->info('Import finished. ' .$i.' members have been imported.'); + } +} diff --git a/app/Services/DolibarrService.php b/app/Services/DolibarrService.php new file mode 100644 index 0000000..0ebd257 --- /dev/null +++ b/app/Services/DolibarrService.php @@ -0,0 +1,61 @@ +baseUrl = config('services.dolibarr.base_url'); + $this->username = config('services.dolibarr.username'); + $this->password = config('services.dolibarr.password'); + $this->apiKey = config('services.dolibarr.api_key'); + } + + /** + * Create an authenticated HTTP client for Dolibarr + */ + protected function client(): PendingRequest + { + return Http::withBasicAuth($this->username, $this->password) + ->withHeaders([ + 'Accept' => 'application/json', + 'DOLAPIKEY' => $this->apiKey, + ]); + } + + /** + * Get all members + * @throws ConnectionException + */ + public function getAllMembers(int $limit = 400, string $sortField = 't.rowid', string $sortOrder = 'ASC'): array + { + $response = $this->client()->get($this->baseUrl . '/members', [ + 'sortfield' => $sortField, + 'sortorder' => $sortOrder, + 'limit' => $limit, + ]); + + return $response->json(); + } + + /** + * Get member subscriptions + * @throws ConnectionException + */ + public function getMemberSubscriptions(int|string $id): array + { + $response = $this->client()->get($this->baseUrl . '/members/'. $id . '/subscriptions'); + + return $response->json(); + } +} diff --git a/app/Services/ISPConfigService.php b/app/Services/ISPConfigService.php new file mode 100644 index 0000000..42599f6 --- /dev/null +++ b/app/Services/ISPConfigService.php @@ -0,0 +1,89 @@ +client = new SoapClient(null, [ + 'location' => config('services.ispconfig' . $type . '.base_url'), + 'trace' => true, + 'exceptions' => true, + 'stream_context' => stream_context_create([ + 'ssl' => [ + 'verify_peer' => false, + 'verify_peer_name' => false, + 'allow_self_signed' => true, + ], + ]), + ]); + + $this->sessionId = $this->client->login($username, $password); + } catch (SoapFault $e) { + throw new Exception("An error occurred : " . $e->getMessage()); + } + } + + /** + * Get all clients + */ + public function getAllClients(string $username = null, string $password = null): array + { + if (!$this->sessionId) { + $this->connect($username, $password); + } + + try { + $clientIds = $this->client->client_get_all($this->sessionId); + $clients = []; + + foreach ($clientIds as $id) { + $details = $this->client->client_get($this->sessionId, (int)$id); + if (!empty($details)) { + $clients[] = (array) $details; + } + } + + return $clients; + } catch (SoapFault $e) { + throw new Exception("An error occurred : " . $e->getMessage()); + } finally { + $this->disconnect(); + } + } + + /** + * Logout + */ + public function disconnect(): void + { + if ($this->client && $this->sessionId) { + try { + $this->client->logout($this->sessionId); + } catch (SoapFault $e) { + Log::info('ISP Config logout succeeded'); + } + } + } +} diff --git a/composer.json b/composer.json index 4abafe0..185a99d 100644 --- a/composer.json +++ b/composer.json @@ -15,7 +15,8 @@ "laravel/framework": "^12.0", "laravel/tinker": "^2.10", "laravel/wayfinder": "^0.1.9", - "spatie/laravel-permission": "^6.21" + "spatie/laravel-permission": "^6.21", + "ext-soap": "*" }, "require-dev": { "barryvdh/laravel-debugbar": "^3.16", diff --git a/config/services.php b/config/services.php index 6182e4b..089882e 100644 --- a/config/services.php +++ b/config/services.php @@ -35,4 +35,24 @@ return [ ], ], + 'dolibarr' => [ + 'base_url' => env('DOLIBARR_URL'), + 'username' => env('DOLIBARR_USERNAME'), + 'password' => env('DOLIBARR_PWD'), + 'api_key' => env('DOLIBARR_APIKEY') + ], + + 'ispconfig' => [ + 'hosting' => [ + 'base_url' => env('HOSTING_ISPAPI_URL'), + 'username' => env('HOSTING_ISPAPI_USERNAME'), + 'password' => env('HOSTING_ISPAPI_PWD'), + ], + 'mailbox' => [ + 'base_url' => env('MAIL_ISPAPI_URL'), + 'username' => env('MAIL_ISPAPI_USERNAME'), + 'password' => env('MAIL_ISPAPI_PWD'), + ] + ] + ]; diff --git a/resources/js/routes/index.ts b/resources/js/routes/index.ts index d012f52..7e7bcee 100644 --- a/resources/js/routes/index.ts +++ b/resources/js/routes/index.ts @@ -526,3 +526,77 @@ membershipForm.head = (options?: RouteQueryOptions): RouteFormDefinition<'get'> }) membership.form = membershipForm + +/** +* @see routes/dev-routes.php:5 +* @route '/call-dolibarr' +*/ +export const callDolibarr = (options?: RouteQueryOptions): RouteDefinition<'get'> => ({ + url: callDolibarr.url(options), + method: 'get', +}) + +callDolibarr.definition = { + methods: ["get","head"], + url: '/call-dolibarr', +} satisfies RouteDefinition<["get","head"]> + +/** +* @see routes/dev-routes.php:5 +* @route '/call-dolibarr' +*/ +callDolibarr.url = (options?: RouteQueryOptions) => { + return callDolibarr.definition.url + queryParams(options) +} + +/** +* @see routes/dev-routes.php:5 +* @route '/call-dolibarr' +*/ +callDolibarr.get = (options?: RouteQueryOptions): RouteDefinition<'get'> => ({ + url: callDolibarr.url(options), + method: 'get', +}) + +/** +* @see routes/dev-routes.php:5 +* @route '/call-dolibarr' +*/ +callDolibarr.head = (options?: RouteQueryOptions): RouteDefinition<'head'> => ({ + url: callDolibarr.url(options), + method: 'head', +}) + +/** +* @see routes/dev-routes.php:5 +* @route '/call-dolibarr' +*/ +const callDolibarrForm = (options?: RouteQueryOptions): RouteFormDefinition<'get'> => ({ + action: callDolibarr.url(options), + method: 'get', +}) + +/** +* @see routes/dev-routes.php:5 +* @route '/call-dolibarr' +*/ +callDolibarrForm.get = (options?: RouteQueryOptions): RouteFormDefinition<'get'> => ({ + action: callDolibarr.url(options), + method: 'get', +}) + +/** +* @see routes/dev-routes.php:5 +* @route '/call-dolibarr' +*/ +callDolibarrForm.head = (options?: RouteQueryOptions): RouteFormDefinition<'get'> => ({ + action: callDolibarr.url({ + [options?.mergeQuery ? 'mergeQuery' : 'query']: { + _method: 'HEAD', + ...(options?.query ?? options?.mergeQuery ?? {}), + } + }), + method: 'get', +}) + +callDolibarr.form = callDolibarrForm diff --git a/routes/dev-routes.php b/routes/dev-routes.php new file mode 100644 index 0000000..a588223 --- /dev/null +++ b/routes/dev-routes.php @@ -0,0 +1,13 @@ +getAllMembers(); + // find specific + $userData = collect($members)->firstWhere('id', 139); + + dd($userData); + +})->name('call-dolibarr'); diff --git a/routes/web.php b/routes/web.php index bd2fcd1..6c9b71d 100644 --- a/routes/web.php +++ b/routes/web.php @@ -17,3 +17,4 @@ Route::middleware(['auth', 'verified'])->group(function () { require __DIR__.'/settings.php'; require __DIR__.'/auth.php'; require __DIR__.'/forms.php'; +require __DIR__.'/dev-routes.php';