preview($request->user()); return $request->expectsJson() ? response()->json(['plan' => $plan])->header('Cache-Control', 'private, no-store') : response()->view('management.editorial-installation', ['plan' => $plan, 'operationKey' => (string) \Illuminate\Support\Str::uuid(), 'receipt' => null])->header('Cache-Control', 'private, no-store'); } public function install(Request $request, \App\Services\EditorialInstallation $installation) { $receipt = $installation->apply($request->user(), $request->all()); return $request->expectsJson() ? response()->json(['receipt' => $receipt])->header('Cache-Control', 'private, no-store') : response()->view('management.editorial-installation', ['plan' => $installation->preview($request->user()), 'operationKey' => (string) \Illuminate\Support\Str::uuid(), 'receipt' => $receipt])->header('Cache-Control', 'private, no-store'); } public function confirmMfa(Request $request) { $actor = $request->user(); abort_unless($actor->two_factor_secret !== null && $actor->two_factor_confirmed_at !== null, 403, 'MFA configurado e confirmado necessário.'); if (app(\App\Services\PortalProfileAccess::class)->mfa($actor)) { return redirect()->route('editorial.installation'); } if ($request->isMethod('get')) { return response()->view('management.editorial-mfa')->header('Cache-Control', 'private, no-store'); } abort_if(array_diff(array_keys($request->all()), ['code', 'recovery_code', '_token']), 422, 'A conta e o destino são obtidos da sessão e do servidor.'); $data = $request->validate(['code' => 'nullable|string|max:32', 'recovery_code' => 'nullable|string|max:255']); try { $recovery = collect($actor->recoveryCodes())->first(fn ($code) => isset($data['recovery_code']) && hash_equals($code, $data['recovery_code'])); $valid = $recovery !== null || (!empty($data['code']) && app(\Laravel\Fortify\Contracts\TwoFactorAuthenticationProvider::class)->verify( \Laravel\Fortify\Fortify::currentEncrypter()->decrypt($actor->two_factor_secret), $data['code'] )); } catch (\Illuminate\Contracts\Encryption\DecryptException $e) { $valid = false; } if (!$valid) { event(new \Laravel\Fortify\Events\TwoFactorAuthenticationFailed($actor)); throw ValidationException::withMessages(['code' => 'Não foi possível confirmar o código MFA.']); } if ($recovery !== null) { $actor->replaceRecoveryCode($recovery); } event(new \Laravel\Fortify\Events\ValidTwoFactorAuthenticationCodeProvided($actor)); $request->session()->regenerate(); return redirect()->route('editorial.installation')->header('Cache-Control', 'private, no-store'); } private function installed(): void { abort_unless(Schema::hasColumn('portal_resources', 'editorial_content'), 503, 'Editor editorial ainda não instalado.'); } private function publicResources() { return DB::table('portal_resources as r')->join('documents as d', 'd.id', '=', 'r.document_id') ->where('r.published', true)->where('r.rights_status', 'authorized')->where('d.rights_status', 'authorized') ->where('d.status', 'active')->where('d.data_classification', 'public')->whereNull('d.deleted_at') ->where('d.malware_scan_status', 'clean')->whereColumn('d.sha256', 'd.malware_scanned_sha256'); } public function edit(int $id) { $this->installed(); $r = DB::table('portal_resources')->find($id); abort_unless($r, 404); $content = $r->editorial_content ? json_decode($r->editorial_content, true) : null; if ($content === null) { $document = DB::table('documents')->find($r->document_id); if ($document && $document->status === 'active' && $document->malware_scan_status === 'clean' && in_array($document->media_type, ['text/plain', 'application/json'], true)) { $binary = app(VerifiedDocumentBinary::class)->respond($document); $bytes = $binary instanceof \Symfony\Component\HttpFoundation\BinaryFileResponse ? file_get_contents($binary->getFile()->getPathname()) : $binary->getContent(); $original = json_decode($bytes, true); if (is_array($original) && ($original['format'] ?? '') === 'investmoz-legacy-article-v1') { $html = preg_replace('~|~i', "\n\n", $original['content_html'] ?? ''); $content = ['body' => trim(html_entity_decode(strip_tags($html), ENT_QUOTES | ENT_HTML5, 'UTF-8')), 'image_resource_id' => null]; } } } return Inertia::render('portal/article-editor', ['resource' => ['id' => $r->id, 'title' => $r->title, 'metadata_version' => $r->metadata_version], 'content' => $content, 'images' => $this->publicResources()->whereIn('d.media_type', ['image/png', 'image/jpeg', 'image/webp'])->get(['r.id', 'r.title']), 'revisions' => DB::table('portal_resource_revisions')->where('resource_id', $id)->orderByDesc('revision_no')->get(['revision_no', 'change_type', 'created_at'])]); } public function save(Request $request, int $id) { $this->installed(); $data = $request->validate(['body' => ['required', 'string', 'max:100000'], 'image_resource_id' => ['nullable', 'integer'], 'metadata_version' => ['required', 'integer', 'min:1']]); DB::transaction(function () use ($request, $id, $data) { $r = DB::table('portal_resources')->where('id', $id)->lockForUpdate()->first(); abort_unless($r, 404); abort_unless((int) $r->metadata_version === (int) $data['metadata_version'], 409, 'O artigo foi alterado noutra sessão. Recarregue antes de editar.'); if (! empty($data['image_resource_id'])) { abort_unless($this->publicResources()->where('r.id', $data['image_resource_id'])->whereIn('d.media_type', ['image/png', 'image/jpeg', 'image/webp'])->exists(), 422, 'Escolha uma imagem pública disponível na biblioteca.'); } $body = trim($data['body']); if ($body === '') { throw ValidationException::withMessages(['body' => 'Escreva o corpo do artigo.']); } $content = ['format' => 1, 'body' => $body, 'image_resource_id' => $data['image_resource_id'] ?? null]; $revision = (int) $r->metadata_version + 1; DB::table('portal_resources')->where('id', $id)->update(['editorial_content' => json_encode($content, JSON_THROW_ON_ERROR), 'published' => false, 'approved_by' => null, 'metadata_version' => $revision, 'last_reviewed_by' => $request->user()->id, 'last_reviewed_at' => now(), 'updated_at' => now()]); $resource = DB::table('portal_resources')->find($id); $links = DB::table('portal_resource_links')->where('resource_id', $id)->orderBy('id')->get(); DB::table('portal_resource_revisions')->insert(['resource_id' => $id, 'revision_no' => $revision, 'change_type' => 'metadata_updated', 'snapshot' => json_encode(['resource' => $resource, 'associations' => $links], JSON_THROW_ON_ERROR), 'actor_user_id' => $request->user()->id, 'created_at' => now()]); DB::table('audit_events')->insert(['source_system' => 'portal-management', 'actor_user_id' => $request->user()->id, 'entity_type' => 'portal_resources', 'entity_id' => null, 'action' => 'editorial_updated', 'before_data' => json_encode(['record_id' => $id, 'metadata_version' => $r->metadata_version]), 'after_data' => json_encode(['record_id' => $id, 'metadata_version' => $revision, 'published' => false])]); }); return redirect('/management/portal-services/resources/'.$id.'/article')->with('success', 'Artigo guardado como rascunho. Publique expressamente no acervo quando estiver pronto.'); } public function show(int $id) { $this->installed(); $r = $this->publicResources()->where('r.id', $id)->first(['r.id', 'r.title', 'r.description', 'r.editorial_content', 'r.document_id', 'd.issuer']); abort_unless($r && $r->editorial_content, 404); $content = json_decode($r->editorial_content, true); // Recheck the original bytes using the same binary gate as the library. $binary = app(VerifiedDocumentBinary::class)->respond($this->publicResources()->where('r.id', $id)->first(['r.*', 'd.*'])); $image = ! empty($content['image_resource_id']) ? $this->publicResources()->where('r.id', $content['image_resource_id'])->whereIn('d.media_type', ['image/png', 'image/jpeg', 'image/webp'])->value('r.id') : null; return Inertia::render('portal/article', ['article' => ['id' => $id, 'title' => $r->title, 'description' => $r->description, 'author' => $r->issuer, 'body' => $content['body'], 'image_url' => $image ? '/documentos/'.$image.'/view' : null, 'original_url' => '/documentos/'.$id]]); } }