feat(116): hostedsms integration settings

Phase 116 complete:
- add HostedSMS settings with encrypted password storage
- add SimpleAPI real test SMS flow and integrations hub row
- document schema, architecture, changelog, and PAUL state

Co-Authored-By: Codex <noreply@openai.com>
This commit is contained in:
2026-05-12 12:25:07 +02:00
parent adacb65110
commit bc2ed2c8e2
20 changed files with 1282 additions and 56 deletions

View File

@@ -0,0 +1,154 @@
<?php
declare(strict_types=1);
namespace App\Modules\Settings;
use App\Core\Exceptions\IntegrationConfigException;
use App\Core\Http\RedirectPathResolver;
use App\Core\Http\Request;
use App\Core\Http\Response;
use App\Core\I18n\Translator;
use App\Core\Security\Csrf;
use App\Core\Support\Flash;
use App\Core\View\Template;
use App\Modules\Auth\AuthService;
use Throwable;
final class HostedSmsIntegrationController
{
public function __construct(
private readonly Template $template,
private readonly Translator $translator,
private readonly AuthService $auth,
private readonly HostedSmsIntegrationRepository $repository,
private readonly HostedSmsApiClient $apiClient,
private readonly IntegrationsRepository $integrations
) {
}
public function index(Request $request): Response
{
$html = $this->template->render('settings/hostedsms', [
'title' => $this->translator->get('settings.hostedsms.title'),
'activeMenu' => 'settings',
'activeSettings' => 'integrations',
'user' => $this->auth->user(),
'csrfToken' => Csrf::token(),
'settings' => $this->repository->getSettings(),
'errorMessage' => (string) Flash::get('settings_error', ''),
'successMessage' => (string) Flash::get('settings_success', ''),
'testMessage' => (string) Flash::get('hostedsms_test', ''),
], 'layouts/app');
return Response::html($html);
}
public function save(Request $request): Response
{
$redirectTo = $this->resolveRedirect($request);
if (!Csrf::validate((string) $request->input('_token', ''))) {
Flash::set('settings_error', $this->translator->get('auth.errors.csrf_expired'));
return Response::redirect($redirectTo);
}
try {
$this->repository->saveSettings([
'user_email' => (string) $request->input('user_email', ''),
'password' => (string) $request->input('password', ''),
'sender' => (string) $request->input('sender', ''),
'convert_message_to_gsm7' => $request->input('convert_message_to_gsm7', ''),
'is_active' => $request->input('is_active', ''),
]);
Flash::set('settings_success', $this->translator->get('settings.hostedsms.flash.saved'));
} catch (Throwable $exception) {
Flash::set(
'settings_error',
$this->translator->get('settings.hostedsms.flash.save_failed') . ' ' . $exception->getMessage()
);
}
return Response::redirect($redirectTo);
}
public function test(Request $request): Response
{
$redirectTo = $this->resolveRedirect($request);
if (!Csrf::validate((string) $request->input('_token', ''))) {
Flash::set('settings_error', $this->translator->get('auth.errors.csrf_expired'));
return Response::redirect($redirectTo);
}
try {
$phone = $this->validatePhone((string) $request->input('phone', ''));
$message = $this->validateMessage((string) $request->input('message', ''));
$credentials = $this->repository->getCredentials();
if ($credentials === null) {
throw new IntegrationConfigException('Najpierw zapisz kompletna konfiguracje HostedSMS.');
}
$result = $this->apiClient->sendSms(
$credentials['user_email'],
$credentials['password'],
$credentials['sender'],
$phone,
$message,
$credentials['convert_message_to_gsm7']
);
$status = $result['ok'] ? 'ok' : 'fail';
$this->integrations->updateTestResult(
$credentials['integration_id'],
$status,
(int) $result['http_code'],
(string) $result['message']
);
if ($result['ok']) {
Flash::set('hostedsms_test', $this->translator->get('settings.hostedsms.flash.test_success', [
'message_id' => (string) $result['message_id'],
]));
} else {
Flash::set('settings_error', $this->translator->get('settings.hostedsms.flash.test_failed') . ' ' . $result['message']);
}
} catch (Throwable $exception) {
Flash::set('settings_error', $this->translator->get('settings.hostedsms.flash.test_failed') . ' ' . $exception->getMessage());
}
return Response::redirect($redirectTo);
}
private function resolveRedirect(Request $request): string
{
return RedirectPathResolver::resolve(
(string) $request->input('return_to', '/settings/integrations/hostedsms'),
['/settings/integrations'],
'/settings/integrations/hostedsms'
);
}
private function validatePhone(string $value): string
{
$phone = preg_replace('/[\s+\-()]/', '', trim($value)) ?? '';
if (preg_match('/^\d{8,15}$/', $phone) !== 1) {
throw new IntegrationConfigException('Podaj numer telefonu w formacie miedzynarodowym, np. 48xxxxxxxxx.');
}
return $phone;
}
private function validateMessage(string $value): string
{
$message = trim($value);
if ($message === '') {
throw new IntegrationConfigException('Podaj tresc testowego SMS.');
}
if (strlen($message) > 4000) {
throw new IntegrationConfigException('Tresc SMS nie moze przekraczac 4000 znakow.');
}
return $message;
}
}