This commit is contained in:
@@ -5,15 +5,24 @@ namespace App\Console\Commands;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Support\Sleep;
|
||||
use Throwable;
|
||||
|
||||
class CheckDevelopmentEnvironment extends Command
|
||||
{
|
||||
protected $signature = 'portal:dev-check';
|
||||
protected $signature = 'portal:dev-check {--wait=0 : Segundos para aguardar uma falha transitória do PostgreSQL}';
|
||||
|
||||
protected $description = 'Verifica o arranque local sem alterar dados ou mostrar credenciais';
|
||||
|
||||
public function handle(): int
|
||||
{
|
||||
$wait = filter_var($this->option('wait'), FILTER_VALIDATE_INT, ['options' => ['min_range' => 0, 'max_range' => 3600]]);
|
||||
if ($wait === false) {
|
||||
$this->error('--wait deve ser um inteiro entre 0 e 3600 segundos.');
|
||||
|
||||
return self::INVALID;
|
||||
}
|
||||
|
||||
$checks = [
|
||||
'PHP >= 8.4.1' => version_compare(PHP_VERSION, '8.4.1', '>='),
|
||||
'Driver PostgreSQL activo' => extension_loaded('pdo_pgsql'),
|
||||
@@ -25,14 +34,17 @@ class CheckDevelopmentEnvironment extends Command
|
||||
];
|
||||
|
||||
if ($checks['Driver PostgreSQL activo'] && $checks['Base PostgreSQL configurada']) {
|
||||
$checks['Ligação à base de dados'] = $this->checkConnection($wait);
|
||||
}
|
||||
|
||||
if ($checks['Ligação à base de dados'] ?? false) {
|
||||
try {
|
||||
DB::selectOne('SELECT 1');
|
||||
$checks['Ligação à base de dados'] = true;
|
||||
$checks['Extensão PostGIS instalada'] = (bool) DB::selectOne("SELECT EXISTS (SELECT 1 FROM pg_extension WHERE extname = 'postgis') AS installed")->installed;
|
||||
$checks['Tabelas principais instaladas'] = collect(['migrations', 'users', 'sectors', 'opportunities', 'sessions', 'cache', 'jobs'])
|
||||
->every(fn (string $table): bool => Schema::hasTable($table));
|
||||
} catch (\Throwable) {
|
||||
$checks['Ligação à base de dados'] = false;
|
||||
} catch (Throwable) {
|
||||
$checks['Esquema da base de dados verificável'] = false;
|
||||
$this->warn('A ligação foi estabelecida, mas a verificação do esquema falhou. Verifique as permissões e a preparação QA.');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,4 +60,61 @@ class CheckDevelopmentEnvironment extends Command
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
private function checkConnection(int $wait): bool
|
||||
{
|
||||
$deadline = microtime(true) + $wait;
|
||||
|
||||
while (true) {
|
||||
try {
|
||||
DB::selectOne('SELECT 1');
|
||||
|
||||
return true;
|
||||
} catch (Throwable $error) {
|
||||
[$reason, $retryable] = $this->connectionFailure($error);
|
||||
$remaining = $deadline - microtime(true);
|
||||
if (! $retryable || $remaining <= 0) {
|
||||
$this->warn($reason);
|
||||
if ($retryable) {
|
||||
$this->warn('Inicie Docker Desktop e execute ./deploy/local/start.ps1. Consulte os registos com docker compose --env-file .env -f deploy/local/compose.yaml logs --tail 40 postgres.');
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->line('[AGUARDA] '.$reason.' Nova tentativa; prazo restante: '.(int) ceil($remaining).' s.');
|
||||
DB::purge();
|
||||
Sleep::for(min(5, $remaining))->seconds();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** @return array{string, bool} */
|
||||
private function connectionFailure(Throwable $error): array
|
||||
{
|
||||
// Inspect the chain internally; never print exception messages, DSNs or credentials.
|
||||
$messages = '';
|
||||
do {
|
||||
$messages .= ' '.strtolower($error->getMessage()).' '.strtolower((string) $error->getCode());
|
||||
$error = $error->getPrevious();
|
||||
} while ($error !== null);
|
||||
|
||||
if (str_contains($messages, '28p01') || str_contains($messages, '28000')
|
||||
|| str_contains($messages, 'password authentication failed') || str_contains($messages, 'no pg_hba.conf entry')) {
|
||||
return ['Autenticação PostgreSQL recusada. Verifique as credenciais e as regras de acesso; aguardar não corrige este erro.', false];
|
||||
}
|
||||
if (str_contains($messages, '3d000') || preg_match('/database .* does not exist/', $messages)) {
|
||||
return ['A base PostgreSQL configurada não existe. Verifique a configuração e a preparação QA.', false];
|
||||
}
|
||||
if (str_contains($messages, '57p03') || str_contains($messages, 'recovery mode')
|
||||
|| str_contains($messages, 'not yet accepting connections') || str_contains($messages, 'database system is starting up')) {
|
||||
return ['PostgreSQL em arranque ou recuperação; aguarde a conclusão sem remover o volume.', true];
|
||||
}
|
||||
if (preg_match('/\b08[0-9a-z]{3}\b/', $messages) || str_contains($messages, 'connection refused')
|
||||
|| str_contains($messages, '57p01') || str_contains($messages, '57p02')) {
|
||||
return ['PostgreSQL indisponível. Verifique se Docker Desktop e o contentor local estão iniciados.', true];
|
||||
}
|
||||
|
||||
return ['Falha PostgreSQL não transitória. Verifique a configuração e os registos do servidor.', false];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Mail\ContactMessageReceivedMail;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Facades\Mail;
|
||||
use Throwable;
|
||||
|
||||
class PortalContactController extends Controller
|
||||
{
|
||||
// Adapter for investidor's contact form: preserve its fields and validation,
|
||||
// using the configured mail transport without adding a database dependency.
|
||||
public function store(Request $request): JsonResponse
|
||||
{
|
||||
$data = $request->validate([
|
||||
'name' => ['required', 'string', 'min:2', 'max:150'],
|
||||
'email' => ['required', 'email', 'max:255'],
|
||||
'phone' => ['nullable', 'string', 'max:30'],
|
||||
'subject' => ['required', 'string', 'min:3', 'max:100'],
|
||||
'message' => ['required', 'string', 'min:10', 'max:2000'],
|
||||
]);
|
||||
|
||||
try {
|
||||
Mail::to(config('contact_messages.recipient'))
|
||||
->send(new ContactMessageReceivedMail([
|
||||
'name' => $data['name'], 'email' => $data['email'], 'phone' => $data['phone'] ?? null,
|
||||
'subject' => $data['subject'], 'message' => $data['message'], 'platform' => 'InvesteMoz',
|
||||
]));
|
||||
} catch (Throwable $exception) {
|
||||
Log::warning('Falha no envio de contacto do portal.', ['exception' => $exception::class]);
|
||||
|
||||
return response()->json(['success' => false, 'message' => 'Ocorreu um erro ao enviar a mensagem.'], 500);
|
||||
}
|
||||
|
||||
return response()->json(['success' => true, 'message' => 'Mensagem enviada com sucesso.'], 201);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mail;
|
||||
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Mail\Mailable;
|
||||
use Illuminate\Mail\Mailables\Address;
|
||||
use Illuminate\Mail\Mailables\Content;
|
||||
use Illuminate\Mail\Mailables\Envelope;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
// Adapted from investidor/app/Mail/ContactMessageReceivedMail.php.
|
||||
class ContactMessageReceivedMail extends Mailable
|
||||
{
|
||||
use Queueable, SerializesModels;
|
||||
|
||||
/** @param array{name: string, email: string, phone?: string|null, subject: string, message: string, platform: string} $contactMessage */
|
||||
public function __construct(public readonly array $contactMessage) {}
|
||||
|
||||
public function envelope(): Envelope
|
||||
{
|
||||
return new Envelope(
|
||||
replyTo: [new Address($this->contactMessage['email'], $this->contactMessage['name'])],
|
||||
subject: $this->contactMessage['subject'],
|
||||
);
|
||||
}
|
||||
|
||||
public function content(): Content
|
||||
{
|
||||
return new Content(
|
||||
view: 'emails.contact-message-received',
|
||||
text: 'emails.contact-message-received-text',
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user