management->installed() || ! $this->access->canView($actor, $record)) { return false; } if ($record->owner_user_id === null) { return $this->access->legacyWriteAllowed($actor); } return $this->access->scoped($actor, 'publisher', $record->organization_id, $record->opportunity_category) && $this->access->operational($actor, 'publish', $record->id) && ! $this->access->conflict($actor, $record) && (int) data_get($record->workflow_context, 'approval.actor_id') !== $actor->id && (! data_get($record->workflow_context, 'publication_assignment.assignee_user_id') || (int) data_get($record->workflow_context, 'publication_assignment.assignee_user_id') === $actor->id); } private function authorize(User $actor, Opportunity $record): void { abort_unless($this->allowed($actor, $record), 403); if ($record->owner_user_id !== null) { app(OpportunityProfileWorkflow::class)->authorizeEditorialCuration($actor, $record); } } /** @return array */ public function state(User $actor, int $id, int $page = 1): array { $record = Opportunity::findOrFail($id); $this->authorize($actor, $record); $history = DB::table('audit_events')->where('source_system', 'opportunity-curation')->where('after_data->record_id', $id) ->orderByDesc('id')->paginate(25, ['id', 'actor_user_id', 'occurred_at', 'after_data'], 'page', $page)->through(function ($row) { $data = json_decode($row->after_data, true, 512, JSON_THROW_ON_ERROR); return ['id' => $row->id, 'actor' => $row->actor_user_id, 'at' => $row->occurred_at, 'featured' => $data['featured'], 'reason' => Crypt::decryptString($data['reason'])]; }); return ['record' => ['id' => $record->id, 'title' => $record->title, 'featured' => $record->is_featured], 'etag' => $this->management->etag($record), 'history' => $history]; } /** @param array $input * @return array */ public function preview(User $actor, int $id, array $input): array { $data = $this->validate($input); $record = Opportunity::findOrFail($id); $this->authorize($actor, $record); $this->check($record, $data); return ['hash' => $this->hash($actor, $id, $data), 'title' => $record->title, 'before' => $record->is_featured, 'featured' => $data['featured'], 'reason' => $data['reason']]; } /** @param array $input * @return array */ public function confirm(User $actor, int $id, array $input): array { $data = $this->validate($input); Validator::make($input, ['hash' => 'required|string|size:64', 'confirm' => 'required|accepted'])->validate(); abort_unless(hash_equals($this->hash($actor, $id, $data), $input['hash']), 409, 'A conferência mudou; prepare nova prévia.'); return DB::transaction(function () use ($actor, $id, $data) { $record = Opportunity::whereKey($id)->lockForUpdate()->firstOrFail(); $this->authorize($actor, $record); $receipt = DB::table('audit_events')->where('source_system', 'opportunity-curation')->where('after_data->record_id', $id) ->where('after_data->client_key', $data['client_key'])->first(['actor_user_id', 'after_data']); $intent = $this->hash($actor, $id, $data); if ($receipt) { $prior = json_decode($receipt->after_data, true, 512, JSON_THROW_ON_ERROR); abort_unless((int) $receipt->actor_user_id === $actor->id && hash_equals($prior['intent'], $intent), 409, 'Chave já utilizada com outro conteúdo.'); return ['replayed' => true, 'featured' => $record->is_featured, 'etag' => $this->management->etag($record)]; } $this->check($record, $data); $before = $record->is_featured; $record->management_version++; PortalAudit::save($record, ['is_featured' => $data['featured']], $actor->id); DB::table('audit_events')->insert(['source_system' => 'opportunity-curation', 'entity_type' => 'opportunities', 'actor_user_id' => $actor->id, 'action' => 'featured_changed', 'data_classification' => 'restricted', 'before_data' => json_encode(['record_id' => $id, 'featured' => $before], JSON_THROW_ON_ERROR), 'after_data' => json_encode(['record_id' => $id, 'featured' => $record->is_featured, 'version' => $record->management_version, 'reason' => Crypt::encryptString($data['reason']), 'client_key' => $data['client_key'], 'intent' => $intent], JSON_THROW_ON_ERROR)]); return ['replayed' => false, 'featured' => $record->is_featured, 'etag' => $this->management->etag($record)]; }); } /** @param array $input * @return array{etag:string,client_key:string,featured:bool,reason:string} */ private function validate(array $input): array { $data = Validator::make($input, ['etag' => 'required|string|size:64', 'client_key' => 'required|uuid', 'featured' => 'required|boolean', 'reason' => 'required|string|min:20|max:1000'])->validate(); return ['etag' => $data['etag'], 'client_key' => $data['client_key'], 'featured' => (bool) $data['featured'], 'reason' => $data['reason']]; } /** @param array{etag:string,client_key:string,featured:bool,reason:string} $data */ private function check(Opportunity $record, array $data): void { abort_unless(hash_equals($this->management->etag($record), $data['etag']), 409, 'A oportunidade mudou; recupere a versão actual antes de confirmar.'); abort_if($record->is_featured === $data['featured'], 422, 'O estado de destaque já corresponde à escolha.'); if ($data['featured']) { abort_unless(Opportunity::published()->whereKey($record->id)->exists(), 409, 'Só uma oportunidade actualmente pública pode entrar em destaque.'); } } /** @param array{etag:string,client_key:string,featured:bool,reason:string} $data */ private function hash(User $actor, int $id, array $data): string { return hash_hmac('sha256', json_encode([$actor->id, $id, $data], JSON_THROW_ON_ERROR), (string) config('app.key')); } }