113 lines
5.1 KiB
Diff
113 lines
5.1 KiB
Diff
--- a/app/Providers/AppServiceProvider.php
|
|
+++ b/app/Providers/AppServiceProvider.php
|
|
@@ -42,6 +42,20 @@
|
|
public function boot(): void
|
|
{
|
|
$this->configureDefaults();
|
|
+
|
|
+ // Fortify calls this response AFTER invalidating/regenerating the server session.
|
|
+ // Clear the client's encryption key too, so Back must reauthorize private pages.
|
|
+ $this->app->bind(\Laravel\Fortify\Contracts\LogoutResponse::class, function () {
|
|
+ return new class extends \Laravel\Fortify\Http\Responses\LogoutResponse
|
|
+ {
|
|
+ public function toResponse($request)
|
|
+ {
|
|
+ \Inertia\Inertia::clearHistory();
|
|
+
|
|
+ return parent::toResponse($request);
|
|
+ }
|
|
+ };
|
|
+ });
|
|
|
|
Event::listen(ValidTwoFactorAuthenticationCodeProvided::class, function ($event): void {
|
|
// The authenticated installers also operate before the optional profile schema exists.
|
|
--- a/app/Http/Controllers/OpportunityResourceController.php
|
|
+++ b/app/Http/Controllers/OpportunityResourceController.php
|
|
@@ -17,6 +17,7 @@
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Facades\URL;
|
|
+use Illuminate\Support\Str;
|
|
use Inertia\Inertia;
|
|
use Inertia\Response;
|
|
use Symfony\Component\HttpFoundation\BinaryFileResponse;
|
|
@@ -41,16 +42,35 @@
|
|
|
|
public function organizationCountryPreview(Request $request, OrganizationCountryInstallation $service): JsonResponse|Response
|
|
{
|
|
- $plan = [...$service->preview($request->user()), 'actionUrl' => '/management/opportunity-resources-install/organization-country', 'extension' => true];
|
|
+ $plan = $service->preview($request->user());
|
|
+ $scope = $this->countryOperationScope($request, $plan);
|
|
+ $operation = $request->session()->get('portal.country_installation_operation');
|
|
+ if (! is_array($operation) || ($operation['scope'] ?? null) !== $scope) {
|
|
+ $operation = ['scope' => $scope, 'key' => (string) Str::uuid()];
|
|
+ $request->session()->put('portal.country_installation_operation', $operation);
|
|
+ }
|
|
+ $plan = [...$plan, 'operationKey' => $operation['key'], 'actionUrl' => '/management/opportunity-resources-install/organization-country', 'extension' => true];
|
|
|
|
return $request->expectsJson() ? response()->json($plan) : Inertia::render('management/opportunity-install', $plan);
|
|
}
|
|
|
|
public function organizationCountryApply(Request $request, OrganizationCountryInstallation $service): JsonResponse|RedirectResponse
|
|
{
|
|
+ // Preview authorizes the actor; the UUID remains stable until this scope changes or logout.
|
|
+ $plan = $service->preview($request->user());
|
|
+ $operation = $request->session()->get('portal.country_installation_operation');
|
|
+ $data = $request->validate(['idempotency_key' => 'required|uuid']);
|
|
+ abort_unless(is_array($operation) && ($operation['scope'] ?? null) === $this->countryOperationScope($request, $plan)
|
|
+ && hash_equals($operation['key'], $data['idempotency_key']), 409, 'A operação não corresponde a esta prévia. Atualize a página antes de tentar novamente.');
|
|
$result = $service->apply($request->user(), $request->all());
|
|
|
|
return $request->expectsJson() ? response()->json($result) : back()->with('success', 'País mestre instalado; nenhuma submissão anterior alterada.');
|
|
+ }
|
|
+
|
|
+ /** @param array<string, mixed> $plan */
|
|
+ private function countryOperationScope(Request $request, array $plan): string
|
|
+ {
|
|
+ return hash('sha256', json_encode([$request->user()->getAuthIdentifier(), $plan['target'], $plan['migration'], $plan['code_hash']], JSON_THROW_ON_ERROR));
|
|
}
|
|
|
|
public function index(int $id, OpportunityResources $service): Response
|
|
--- a/resources/js/pages/management/opportunity-install.tsx
|
|
+++ b/resources/js/pages/management/opportunity-install.tsx
|
|
@@ -12,6 +12,7 @@
|
|
actionUrl = '/management/opportunity-management-install',
|
|
imageProcessingAvailable,
|
|
extension = false,
|
|
+ operationKey,
|
|
}: {
|
|
installed: boolean;
|
|
ready: boolean;
|
|
@@ -24,11 +25,12 @@
|
|
actionUrl?: string;
|
|
imageProcessingAvailable?: boolean;
|
|
extension?: boolean;
|
|
+ operationKey?: string;
|
|
}) {
|
|
const form = useForm({
|
|
hash,
|
|
confirm: false,
|
|
- idempotency_key: crypto.randomUUID(),
|
|
+ idempotency_key: operationKey ?? '',
|
|
reason: '',
|
|
});
|
|
return (
|
|
@@ -113,6 +115,7 @@
|
|
installed ||
|
|
!ready ||
|
|
!form.data.confirm ||
|
|
+ (extension && !operationKey) ||
|
|
form.processing
|
|
}
|
|
>
|
|
@@ -120,6 +123,9 @@
|
|
? 'Aplicar extensão do país'
|
|
: 'Aplicar plano de gestão'}
|
|
</button>
|
|
+ {extension && !operationKey && (
|
|
+ <p role="alert">Atualize a prévia para obter a chave desta operação.</p>
|
|
+ )}
|
|
{!ready && (
|
|
<p>
|
|
Aplicação indisponível até comprovar QA,
|