@@ -23,13 +23,18 @@ final class SimulationFormDrafts
|
||||
public function create(array $data, Request $request, string $key): array
|
||||
{
|
||||
[$actorId, $tokenHash] = $this->context($request);
|
||||
$caseId = $request->input('context');
|
||||
if ($caseId !== null) {
|
||||
abort_unless(config('investor-path.enabled') && is_string($caseId) && Str::isUuid($caseId), 422, 'Contexto de investimento indisponível.');
|
||||
abort_unless($request->user()?->hasVerifiedEmail(), 403);
|
||||
}
|
||||
$data = SimulationFormDraftPrivacyPolicy::apply($data, $actorId !== null);
|
||||
$scope = $actorId === null ? 'anonymous:'.$tokenHash : 'user:'.$actorId;
|
||||
$scope = ($actorId === null ? 'anonymous:'.$tokenHash : 'user:'.$actorId).($caseId ? ':case:'.$caseId : '');
|
||||
$keyHash = hash_hmac('sha256', strtolower($key), (string) config('app.key'));
|
||||
$canonicalPayload = json_encode($this->canonical($data), JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE);
|
||||
$payloadHash = hash('sha256', $canonicalPayload);
|
||||
|
||||
return DB::transaction(function () use ($actorId, $tokenHash, $scope, $keyHash, $payloadHash, $data): array {
|
||||
return DB::transaction(function () use ($actorId, $tokenHash, $scope, $keyHash, $payloadHash, $data, $caseId, $request): array {
|
||||
$this->setContext($actorId, $tokenHash);
|
||||
DB::select('select pg_advisory_xact_lock(261010, hashtext(?))', [$scope]);
|
||||
|
||||
@@ -39,7 +44,13 @@ final class SimulationFormDrafts
|
||||
->update(['deleted_at' => now()]);
|
||||
}
|
||||
|
||||
$draft = $this->ownedQuery($actorId, $tokenHash)->lockForUpdate()->first();
|
||||
$case = $caseId ? app(VirtualDesk::class)->case($request->user(), $caseId, true, true) : null;
|
||||
// Shared case access never grants ownership of a private simulator snapshot.
|
||||
abort_if($case && ((int) $case->user_id !== $actorId || $case->closed_at), 403);
|
||||
$query = $this->ownedQuery($actorId, $tokenHash);
|
||||
$draft = $case
|
||||
? ($case->simulation_id ? $query->whereKey($case->simulation_id)->lockForUpdate()->firstOrFail() : null)
|
||||
: $this->unscoped($query, $actorId)->lockForUpdate()->first();
|
||||
if ($draft) {
|
||||
if (hash_equals($draft->create_key_hash, $keyHash)) {
|
||||
abort_unless(hash_equals($draft->create_payload_hash, $payloadHash), 409, 'Idempotency-Key já foi utilizada com outros dados.');
|
||||
@@ -57,6 +68,11 @@ final class SimulationFormDrafts
|
||||
'expires_at' => $actorId === null ? now()->addDays(30) : null,
|
||||
]);
|
||||
|
||||
if ($case) {
|
||||
DB::table('desk_cases')->where('id', $case->id)->update(['simulation_id' => $draft->id, 'version' => $case->version + 1, 'updated_at' => now()]);
|
||||
app(VirtualDesk::class)->event(app(VirtualDesk::class)->case($request->user(), $case->id), $request->user(), 'context_draft_created', ['draft_id' => $draft->id]);
|
||||
}
|
||||
|
||||
return ['draft' => $draft, 'replayed' => false];
|
||||
}, 3);
|
||||
}
|
||||
@@ -70,6 +86,7 @@ final class SimulationFormDrafts
|
||||
return DB::transaction(function () use ($id, $data, $request, $actorId, $tokenHash): SimulationFormDraft {
|
||||
$this->setContext($actorId, $tokenHash);
|
||||
$draft = $this->ownedQuery($actorId, $tokenHash)->lockForUpdate()->findOrFail($id);
|
||||
$this->checkContext($request, $draft);
|
||||
$this->requireMatch($request, $draft);
|
||||
$previousEconomic = SimulationFormDraft::decryptSnapshot($draft->economic_snapshot);
|
||||
if (($data['economic_snapshot']['reuse_simulation'] ?? null) === 'no') {
|
||||
@@ -162,10 +179,19 @@ final class SimulationFormDrafts
|
||||
{
|
||||
[$actorId, $tokenHash] = $this->context($request);
|
||||
|
||||
return DB::transaction(function () use ($actorId, $tokenHash): ?SimulationFormDraft {
|
||||
return DB::transaction(function () use ($actorId, $tokenHash, $request): ?SimulationFormDraft {
|
||||
$this->setContext($actorId, $tokenHash);
|
||||
$query = $this->ownedQuery($actorId, $tokenHash);
|
||||
if ($request->query('context') !== null) {
|
||||
$id = $request->query('context');
|
||||
abort_unless(config('investor-path.enabled') && $request->user()?->hasVerifiedEmail() && is_string($id) && Str::isUuid($id), 403);
|
||||
$case = app(VirtualDesk::class)->case($request->user(), $id, true);
|
||||
abort_unless((int) $case->user_id === $actorId, 403);
|
||||
|
||||
return $this->ownedQuery($actorId, $tokenHash)->orderByDesc('updated_at')->first();
|
||||
return $case->simulation_id ? $query->whereKey($case->simulation_id)->firstOrFail() : null;
|
||||
}
|
||||
|
||||
return $this->unscoped($query, $actorId)->orderByDesc('updated_at')->first();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -327,10 +353,12 @@ final class SimulationFormDrafts
|
||||
{
|
||||
[$actorId, $tokenHash] = $this->context($request);
|
||||
|
||||
return DB::transaction(function () use ($id, $actorId, $tokenHash): SimulationFormDraft {
|
||||
return DB::transaction(function () use ($id, $actorId, $tokenHash, $request): SimulationFormDraft {
|
||||
$this->setContext($actorId, $tokenHash);
|
||||
$draft = $this->ownedQuery($actorId, $tokenHash)->findOrFail($id);
|
||||
$this->checkContext($request, $draft);
|
||||
|
||||
return $this->ownedQuery($actorId, $tokenHash)->findOrFail($id);
|
||||
return $draft;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -414,6 +442,31 @@ final class SimulationFormDrafts
|
||||
: $query->where('owner_user_id', $actorId)->whereNull('anonymous_token_hash');
|
||||
}
|
||||
|
||||
/** @param Builder<SimulationFormDraft> $query
|
||||
* @return Builder<SimulationFormDraft>
|
||||
*/
|
||||
private function unscoped(Builder $query, ?int $actorId): Builder
|
||||
{
|
||||
if ($actorId !== null && config('investor-path.enabled') && app(VirtualDesk::class)->installed()) {
|
||||
// The direct tool has its own current draft; context drafts are opened explicitly by ID/context.
|
||||
$query->whereNotExists(fn ($q) => $q->selectRaw('1')->from('desk_cases')
|
||||
->whereColumn('desk_cases.simulation_id', 'simulation_form_drafts.id'));
|
||||
}
|
||||
|
||||
return $query;
|
||||
}
|
||||
|
||||
private function checkContext(Request $request, SimulationFormDraft $draft): void
|
||||
{
|
||||
$context = $request->input('context');
|
||||
if ($context === null) {
|
||||
return;
|
||||
}
|
||||
abort_unless(config('investor-path.enabled') && $request->user()?->hasVerifiedEmail() && is_string($context) && Str::isUuid($context), 403);
|
||||
$case = app(VirtualDesk::class)->case($request->user(), $context, true);
|
||||
abort_unless((int) $case->user_id === $request->user()->id && $case->simulation_id === $draft->id, 403, 'Rascunho pertencente a outro contexto.');
|
||||
}
|
||||
|
||||
/** @return array{id:string,title:string,updated_at:string|null,expires_at:string|null,version_no:int} */
|
||||
private function summary(SimulationFormDraft $draft): array
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user