diff --git a/bootstrap.php b/bootstrap.php index 1298d85..b993d11 100644 --- a/bootstrap.php +++ b/bootstrap.php @@ -11,7 +11,7 @@ use Core\RateLimiter\FileRateLimiter; use Core\RateLimiter\InMemoryRateLimiter; use Psr\Log\LoggerInterface; use Core\LoggerFactory; -use Services\ExternalApiService; +use ElixForms\ElixFormsClient; $container = new Container(); @@ -51,9 +51,11 @@ $container->singleton(LoggerInterface::class, function($c) { return LoggerFactory::create($config->get('log_ident', 'api')); }); -// External API service binding -$container->singleton(ExternalApiService::class, function($c) { - return new ExternalApiService(); +// ElixForms client binding +$container->singleton(ElixFormsClient::class, function($c) { + $config = $c->make(Config::class); + $baseUrl = $config->get('elixforms_api_base_url'); + return new ElixFormsClient($baseUrl); }); // If you have other services, bind them here, for example: diff --git a/composer.json b/composer.json index eaae99d..ccab687 100644 --- a/composer.json +++ b/composer.json @@ -10,10 +10,12 @@ }, "autoload": { "psr-4": { - "Core\\": "src/Core/", - "Controllers\\": "src/Controllers/", - "Services\\": "src/Services/", - "Helpers\\": "src/Helpers/" + "Core\\": "src/Api/Core/", + "Controllers\\": "src/Api/Controllers/", + "Services\\": "src/Api/Services/", + "Helpers\\": "src/Api/Helpers/", + "Api\\": "src/Api/", + "ElixForms\\": "src/ElixForms/" } }, "autoload-dev": { diff --git a/config/config.php.template b/config/config.php.template index 701a592..17f0b3e 100644 --- a/config/config.php.template +++ b/config/config.php.template @@ -1,6 +1,6 @@ 'https://api.example.com', + 'elixforms_api_base_url' => 'https://api.example.com', 'rate_limiter_driver' => 'file', // o 'memory' // configuration for file-based rate limiter 'rate_limit_storage_dir' => sys_get_temp_dir() . '/api_rate_limit', diff --git a/config/secrets.php.template b/config/secrets.php.template index babdcd4..4193a63 100644 --- a/config/secrets.php.template +++ b/config/secrets.php.template @@ -1,6 +1,8 @@ 'myUser', - 'external_api_password' => 'myPass', - 'external_api_token' => 'mySecretToken', + // elixforms API credentials + 'elixforms_api_username' => 'myUser', + 'elixforms_api_password' => 'myPass', + 'elixforms_api_token' => 'mySecretToken', + 'api_access_token' => 'myApiAccessToken', ]; diff --git a/public/index.php b/public/index.php index f276ed9..689810f 100644 --- a/public/index.php +++ b/public/index.php @@ -1,6 +1,7 @@ make(RateLimiterInterface::class); $router = new Router($container); // vedi nota: router può ricevere container +$request = new Request(); +$response = new Response(); + +// Autenticazione separata per le API esterne +if (strpos($request->path(), '/api/') === 0) { + try { + $authenticator = $container->make(ApiTokenAuthenticator::class); + $authenticator->authenticate($request); + } catch (\Throwable $e) { + $logger->warning('External API auth failed', ['path' => $request->path(), 'error' => $e->getMessage()]); + $response->json(['error' => 'Unauthorized'], 401); + } +} + // register routes (path without version prefix) $router->get('/users', 'UsersController@index'); $router->post('/users', 'UsersController@create'); $router->get('/example', 'ExampleController@test'); -$request = new Request(); -$response = new Response(); +// external API routes +$router->get('/api/elixforms/status', 'Api\\Controllers\\ExternalElixFormsController@status'); +$router->post('/api/elixforms/login', 'Api\\Controllers\\ExternalElixFormsController@login'); // Rate limiting by IP address $key = $_SERVER['REMOTE_ADDR'] ?? 'unknown'; diff --git a/src/Api/Auth/ApiTokenAuthenticator.php b/src/Api/Auth/ApiTokenAuthenticator.php new file mode 100644 index 0000000..6c9ca3a --- /dev/null +++ b/src/Api/Auth/ApiTokenAuthenticator.php @@ -0,0 +1,34 @@ +config = $config; + } + + public function authenticate(Request $request): void + { + $authorization = $_SERVER['HTTP_AUTHORIZATION'] ?? $_SERVER['REDIRECT_HTTP_AUTHORIZATION'] ?? ''; + if (!$authorization) { + throw new \Exception('Missing Authorization header'); + } + + if (!preg_match('/^Bearer\s+(.*)$/i', trim($authorization), $matches)) { + throw new \Exception('Invalid Authorization header format'); + } + + $token = $matches[1]; + $expected = $this->config->secret('api_access_token'); + + if (empty($expected) || !hash_equals((string) $expected, (string) $token)) { + throw new \Exception('Invalid API access token'); + } + } +} diff --git a/src/Api/Controllers/ExampleController.php b/src/Api/Controllers/ExampleController.php new file mode 100644 index 0000000..aef2890 --- /dev/null +++ b/src/Api/Controllers/ExampleController.php @@ -0,0 +1,26 @@ +elixFormsClient = $elixFormsClient; + } + + public function test(Request $req, Response $res) + { + $response = $this->elixFormsClient->request('GET', '/status'); + + return $res->json([ + 'external_api_response' => $response['json'] ?? $response['body'], + 'status' => $response['status'] + ]); + } +} diff --git a/src/Api/Controllers/ExternalElixFormsController.php b/src/Api/Controllers/ExternalElixFormsController.php new file mode 100644 index 0000000..faf0e89 --- /dev/null +++ b/src/Api/Controllers/ExternalElixFormsController.php @@ -0,0 +1,44 @@ +elixFormsClient = $elixFormsClient; + } + + public function status(Request $req, Response $res) + { + try { + $response = $this->elixFormsClient->request('GET', '/status'); + return $res->json([ + 'status' => $response['status'], + 'body' => $response['json'] ?? $response['body'] + ]); + } catch (ElixFormsException $e) { + return $res->json(['error' => $e->getMessage()], 500); + } + } + + public function login(Request $req, Response $res) + { + $data = $req->body(); + $username = $data['username'] ?? ''; + $password = $data['password'] ?? ''; + + try { + $token = $this->elixFormsClient->auth()->login($username, $password); + return $res->json(['authToken' => $token]); + } catch (ElixFormsException $e) { + return $res->json(['error' => $e->getMessage()], 401); + } + } +} diff --git a/src/Controllers/v1/UsersController.php b/src/Api/Controllers/V1/UsersController.php similarity index 100% rename from src/Controllers/v1/UsersController.php rename to src/Api/Controllers/V1/UsersController.php diff --git a/src/Core/Config.php b/src/Api/Core/Config.php similarity index 100% rename from src/Core/Config.php rename to src/Api/Core/Config.php diff --git a/src/Core/Container.php b/src/Api/Core/Container.php similarity index 100% rename from src/Core/Container.php rename to src/Api/Core/Container.php diff --git a/src/Core/ElixFormsException.php b/src/Api/Core/ElixFormsException.php similarity index 100% rename from src/Core/ElixFormsException.php rename to src/Api/Core/ElixFormsException.php diff --git a/src/Core/HttpClient.php b/src/Api/Core/HttpClient.php similarity index 100% rename from src/Core/HttpClient.php rename to src/Api/Core/HttpClient.php diff --git a/src/Core/LoggerFactory.php b/src/Api/Core/LoggerFactory.php similarity index 100% rename from src/Core/LoggerFactory.php rename to src/Api/Core/LoggerFactory.php diff --git a/src/Core/RateLimiter/FileRateLimiter.php b/src/Api/Core/RateLimiter/FileRateLimiter.php similarity index 100% rename from src/Core/RateLimiter/FileRateLimiter.php rename to src/Api/Core/RateLimiter/FileRateLimiter.php diff --git a/src/Core/RateLimiter/InMemoryRateLimiter.php b/src/Api/Core/RateLimiter/InMemoryRateLimiter.php similarity index 100% rename from src/Core/RateLimiter/InMemoryRateLimiter.php rename to src/Api/Core/RateLimiter/InMemoryRateLimiter.php diff --git a/src/Core/RateLimiter/RateLimiterInterface.php b/src/Api/Core/RateLimiter/RateLimiterInterface.php similarity index 100% rename from src/Core/RateLimiter/RateLimiterInterface.php rename to src/Api/Core/RateLimiter/RateLimiterInterface.php diff --git a/src/Core/Request.php b/src/Api/Core/Request.php similarity index 65% rename from src/Core/Request.php rename to src/Api/Core/Request.php index ed4417e..fba831b 100644 --- a/src/Core/Request.php +++ b/src/Api/Core/Request.php @@ -17,4 +17,10 @@ class Request { public function query() { return $_GET; } + + public function header(string $name): ?string + { + $key = 'HTTP_' . strtoupper(str_replace('-', '_', $name)); + return $_SERVER[$key] ?? null; + } } diff --git a/src/Core/Response.php b/src/Api/Core/Response.php similarity index 100% rename from src/Core/Response.php rename to src/Api/Core/Response.php diff --git a/src/Core/Router.php b/src/Api/Core/Router.php similarity index 100% rename from src/Core/Router.php rename to src/Api/Core/Router.php diff --git a/src/Helpers/Json.php b/src/Api/Helpers/Json.php similarity index 100% rename from src/Helpers/Json.php rename to src/Api/Helpers/Json.php diff --git a/src/Services/ExternalApiService.php b/src/Api/Services/ExternalApiService.php similarity index 72% rename from src/Services/ExternalApiService.php rename to src/Api/Services/ExternalApiService.php index f0292a8..d22ba8d 100644 --- a/src/Services/ExternalApiService.php +++ b/src/Api/Services/ExternalApiService.php @@ -17,8 +17,8 @@ class ExternalApiService public function getStatus() { - $url = rtrim($this->config->get('external_api_base_url'), '/') . '/status'; - $token = $this->config->secret('external_api_token'); + $url = rtrim($this->config->get('elixforms_api_base_url'), '/') . '/status'; + $token = $this->config->secret('elixforms_api_token'); $headers = ['Authorization' => 'Bearer ' . $token]; return $this->httpClient->get($url, $headers); } diff --git a/src/Controllers/ExampleController.php b/src/Controllers/ExampleController.php deleted file mode 100644 index b502f1f..0000000 --- a/src/Controllers/ExampleController.php +++ /dev/null @@ -1,33 +0,0 @@ -config = $config; - $this->httpClient = $httpClient; - } - - public function test(Request $req, Response $res) - { - - $url = $this->config->get('external_api_base_url') . '/status'; - - $response = $this->httpClient->get($url, [ - 'Authorization' => 'Bearer ' . $this->config->secret('external_api_token') - ]); - - return $res->json([ - 'external_api_response' => $response - ]); - } -} diff --git a/src/ElixForms/Auth/ElixFormsAuthenticationClient.php b/src/ElixForms/Auth/ElixFormsAuthenticationClient.php new file mode 100644 index 0000000..1181ac9 --- /dev/null +++ b/src/ElixForms/Auth/ElixFormsAuthenticationClient.php @@ -0,0 +1,87 @@ +baseUrl = rtrim($baseUrl, '/'); + $this->httpClient = $httpClient ?? new Client(['timeout' => 10.0, 'http_errors' => false]); + } + + /** + * Esegue il login verso l'API di elixForms. + * + * @param string $username + * @param string $password + * @return string Il token di autenticazione (authToken) + * @throws ElixFormsException Se le credenziali sono errate o c'è un errore server + */ + public function login(string $username, string $password): string { + $url = $this->baseUrl . '/eF/services/api/authentication/login/v1'; + + try { + $response = $this->httpClient->post($url, [ + 'form_params' => [ + 'username' => $username, + 'password' => $password + ] + ]); + + $status = $response->getStatusCode(); + if ($status !== 200) { + throw new ElixFormsException("Errore durante il login elixForms. HTTP Status: " . $status); + } + + $body = $response->getBody()->getContents(); + $json = json_decode($body, true); + + if (json_last_error() !== JSON_ERROR_NONE || empty($json)) { + throw new ElixFormsException("Risposta non valida dal server elixForms: atteso JSON."); + } + + if (isset($json['value']['authToken'])) { + return $json['value']['authToken']; + } + + throw new ElixFormsException("authToken non trovato nella risposta del login elixForms."); + } catch (GuzzleException $e) { + throw new ElixFormsException("Errore di connessione al server elixForms: " . $e->getMessage(), 0, $e); + } + } + + /** + * Effettua il logout invalidando il token sul server elixForms. + * + * @param string $username + * @param string $token + * @return bool True se il logout ha successo + * @throws ElixFormsException Se c'è un errore durante il logout + */ + public function logout(string $username, string $token): bool { + $url = $this->baseUrl . '/eF/services/api/authentication/' . urlencode($username) . '/logout/v1'; + + try { + $response = $this->httpClient->post($url, [ + 'headers' => [ + 'Authorization' => 'Bearer ' . $token, + 'Content-Type' => 'application/x-www-form-urlencoded' + ] + ]); + + $status = $response->getStatusCode(); + if ($status !== 200) { + throw new ElixFormsException("Errore durante il logout elixForms. HTTP Status: " . $status); + } + + return true; + } catch (GuzzleException $e) { + throw new ElixFormsException("Errore di connessione al server elixForms: " . $e->getMessage(), 0, $e); + } + } +} diff --git a/src/ElixForms/ElixFormsClient.php b/src/ElixForms/ElixFormsClient.php new file mode 100644 index 0000000..6dc5413 --- /dev/null +++ b/src/ElixForms/ElixFormsClient.php @@ -0,0 +1,52 @@ +baseUrl = rtrim($baseUrl, '/'); + $this->httpClient = $httpClient ?? new Client(['timeout' => 10.0, 'http_errors' => false]); + } + + public function auth(): AuthClient + { + return new AuthClient($this->baseUrl, $this->httpClient); + } + + public function request(string $method, string $path, ?array $body = null, array $headers = []): array + { + $url = $this->baseUrl . '/' . ltrim($path, '/'); + $options = ['headers' => $headers]; + + if ($body !== null) { + $options['json'] = $body; + } + + try { + $response = $this->httpClient->request($method, $url, $options); + $status = $response->getStatusCode(); + $bodyRaw = $response->getBody()->getContents(); + $contentType = $response->getHeaderLine('Content-Type'); + $isJson = stripos($contentType, 'application/json') !== false; + + return [ + 'status' => $status, + 'headers' => $response->getHeaders(), + 'body' => $bodyRaw, + 'is_json' => $isJson, + 'json' => $isJson ? json_decode($bodyRaw, true) : null, + ]; + } catch (GuzzleException $e) { + throw new ElixFormsException('Errore di connessione al server elixForms: ' . $e->getMessage(), 0, $e); + } + } +} diff --git a/src/ElixForms/Exceptions/ElixFormsException.php b/src/ElixForms/Exceptions/ElixFormsException.php new file mode 100644 index 0000000..b074409 --- /dev/null +++ b/src/ElixForms/Exceptions/ElixFormsException.php @@ -0,0 +1,7 @@ +config = $config; - $this->httpClient = $httpClient; - } - - /** - * Esegue il login verso l'API di elixForms. - * - * @param string $username - * @param string $password - * @return string Il token di autenticazione (authToken) - * @throws ElixFormsException Se le credenziali sono errate o c'è un errore server - */ - public function login(string $username, string $password): string { - $baseUrl = rtrim($this->config->get('elixforms_api_base_url'), '/'); - $url = $baseUrl . '/eF/services/api/authentication/login/v1'; - - $data = [ - 'username' => $username, - 'password' => $password - ]; - - // L'API supporta application/json - $response = $this->httpClient->post($url, $data); - - if ($response['status'] !== 200) { - throw new ElixFormsException("Errore durante il login elixForms. HTTP Status: " . $response['status']); - } - - if (!$response['is_json'] || empty($response['json'])) { - throw new ElixFormsException("Risposta non valida dal server elixForms: atteso JSON."); - } - - $json = $response['json']; - - // Estraiamo il token dalla struttura complessa - if (isset($json['value']['authToken'])) { - return $json['value']['authToken']; - } - - throw new ElixFormsException("authToken non trovato nella risposta del login elixForms."); - } - - /** - * Effettua il logout invalidando il token sul server elixForms. - * - * @param string $username - * @param string $token - * @return bool True se il logout ha successo - * @throws ElixFormsException Se c'è un errore durante il logout - */ - public function logout(string $username, string $token): bool { - $baseUrl = rtrim($this->config->get('elixforms_api_base_url'), '/'); - $url = $baseUrl . '/eF/services/api/authentication/' . urlencode($username) . '/logout/v1'; - - $headers = [ - 'Authorization' => 'Bearer ' . $token, - 'Content-Type' => 'application/x-www-form-urlencoded' - ]; - - $response = $this->httpClient->post($url, null, $headers); - - if ($response['status'] !== 200) { - throw new ElixFormsException("Errore durante il logout elixForms. HTTP Status: " . $response['status']); - } - - return true; - } -}