# Gemini — Rotas e Controllers

> Apenas **uma** rota existe para o módulo na v5. O caminho de mascaramento (caminho B) é
> invocado internamente por controllers do portal e **não** expõe endpoint.

---

## 1. Tabela de rotas

| Método | URI | Nome (`->name()`) | Controller@action | Middleware | Arquivo |
|---|---|---|---|---|---|
| `POST` | `/preencher-formulario` | `gemini.ia` | `Prefecture\GeminiController@preencher` | `web` (grupo global) + `check.site.manutention` | `v5/routes/web.php:203` |

Trecho real de `v5/routes/web.php:202-203`:

```php
    ###IA do Gemini ####################################################################
    Route::post('/preencher-formulario', 'Prefecture\GeminiController@preencher')->name('gemini.ia');
```

### 1.1 Groupo de middleware onde a rota vive

`v5/routes/web.php:5` abre o único grupo do arquivo:

```php
Route::middleware(['check.site.manutention'])->group(function () {
    ...
});   // v5/routes/web.php:213
```

A rota `gemini.ia` está **dentro** desse grupo (linha 203 < 213). O grupo `web` é
aplicado globalmente em `v5/app/Providers/RouteServiceProvider.php:54-56`
(`Route::middleware('web')->group(base_path('routes/web.php'))`).

### 1.2 Middleware efetivo da requisição

| Middleware | Origem | Efeito na rota `gemini.ia` |
|---|---|---|
| `EncryptCookies` | grupo `web` (`v5/app/Http/Kernel.php:31`) | padrão |
| `AddQueuedCookiesToResponse` | grupo `web` | padrão |
| `StartSession` | grupo `web` | cria sessão (e portanto um token CSRF) |
| `ShareErrorsFromSession` | grupo `web` | padrão |
| `VerifyCsrfToken` | grupo `web` (`v5/app/Http/Kernel.php:36`) | **exige** `X-CSRF-TOKEN` válido no POST |
| `SubstituteBindings` | grupo `web` | padrão |
| `check.site.manutention` | grupo de `v5/routes/web.php` | se `pagina_em_manutencao = 1` **e** o visitante não estiver logado, devolve a view de manutenção |
| `check.logged` | **AUSENTE** | **nenhum** — a rota é pública |
| `check.route.permission` | **AUSENTE** | **nenhum** — não há verificação de papel/permissão |
| `throttle:*` | **AUSENTE** | **nenhum** — o grupo `api` tem `throttle:60,1`, mas o grupo `web` **não tem** |

---

## 2. RISCO — rota pública sem autenticação

Esta é a **achado mais grave** do módulo e precisa ser tratado explicitamente na v7.

**O que acontece:**

1. A rota está no arquivo de rotas **do portal** (`v5/routes/web.php`), não do CMS
   (`v5/routes/basepro.php`).
2. Ela **não** tem `check.logged` nem `check.route.permission`.
3. Ela **não** tem `throttle`.
4. O `GeminiController` estende `App\Http\Controllers\Admin\Controller` — a herança sugere
   que era uma rota de CMS, mas o registro no portal a deixou pública.
5. O `GeminiPromptRequest::authorize()` retorna `true` — **não há autorização**.

**Consequência prática:** qualquer visitante anônimo do portal pode:
- obter um token CSRF válido em qualquer página pública (o token é vinculado à sessão,
  e `csrf_token()` é impresso no HTML das views do portal);
- enviar `POST /preencher-formulario` com um prompt arbitrário;
- consumir a **chave da API paga da prefeitura**, quantas vezes quiser, até esgotar
  a cota ou gerar custo.

O CSRF **não** mitiga isso: ele protege contra *cross-site*, não contra *unauthenticated
same-site*. O próprio `ia-modal.js` mostra o padrão de uso — o cabeçalho
`X-CSRF-TOKEN` é obtido de `<meta name="csrf-token" content="{{ csrf_token() }}">`.

**Agravantes:**
- O prompt do usuário vai **direto** para o modelo, sem sanitização → *prompt injection*
  por parte de qualquer visitante anônimo.
- A resposta do modelo é devolvida ao visitante (em JSON), o que também **vaza
  informação do modelo** (o modelo pode recusar, ou produzir texto que revele a
  instrução de sistema).
- Sem timeout (`GeminiController` não configura `timeout`), uma requisição pendente pode
  ocupar uma conexão do worker indefinidamente.
- A chave da API viaja na **query string** (`?key=`), ficando registrada em logs de
  acesso do nginx/apache e de proxies.

**Correção mínima para a v7:** mover a rota para o grupo do CMS, exigir
`auth`/`check.logged`, aplicar `throttle` (ex.: `throttle:20,1` por usuário) e colocar a
chave no **header** `x-goog-api-key` em vez da query string.

---

## 3. Controller — `GeminiController@preencher`

Arquivo: `v5/app/Http/Controllers/Prefecture/GeminiController.php`

```php
namespace App\Http\Controllers\Prefecture;

use Illuminate\Http\Request;                 // import NÃO utilizado
use App\Http\Controllers\Admin\Controller;
use App\Prefecture\Configuration;
use App\Http\Requests\GeminiPromptRequest;

class GeminiController extends Controller
{
    public function preencher(GeminiPromptRequest $request)
    {
        $prompt = $request->input('prompt');

        $instrucaoAdicional = "Por favor, forneça a resposta em texto corrido, sem formatação Markdown
        (como negrito, itálico, títulos ou listas). Separe o título do corpo do texto com uma linha em branco.
        Não inclua instruções, comandos ou frases como 'adicione tal coisa' ou 'faça isso'.
        Apenas forneça o texto final, pronto para uso, baseado nos dados fornecidos. O titulo deve ter no máximo 150 caracteres.";

        $promptParaIA = $prompt . "\n\n" . $instrucaoAdicional;

        $apiKey = Configuration::where('key', 'gemini_api_key')->value('value');

        if (empty($apiKey)) {
            return response()->json([
                'title' => 'Erro de Configuração',
                'text' => 'A chave da API do Google Gemini não está configurada ou foi lida incorretamente.',
            ], 500);
        }

        $url = 'https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key=' . $apiKey;
        ...
```

### 3.1 Métodos

| Método | Visibilidade | Responsabilidade |
|---|---|---|
| `preencher` | `public` | Único método. Lê o prompt, monta a instrução, resolve a chave, chama a API, parte a resposta em título/corpo e devolve JSON. |

### 3.2 Post-processing da resposta (`GeminiController.php:53-80`)

```php
$fullText = $body['candidates'][0]['content']['parts'][0]['text'];

$title = '';
$text = '';

$separatorPos = strpos($fullText, "\n\n");

if ($separatorPos !== false) {
    $title = trim(substr($fullText, 0, $separatorPos));
    $text  = trim(substr($fullText, $separatorPos));
} else {
    $firstSentenceEnd = strpos($fullText, '.');
    if ($firstSentenceEnd !== false && $firstSentenceEnd < 150) {
        $title = trim(substr($fullText, 0, $firstSentenceEnd + 1));
        $text  = trim(substr($fullText, $firstSentenceEnd + 1));
    } else {
        $title = trim(substr($fullText, 0, 150));
        $text  = trim(substr($fullText, 150));
        if (strlen($fullText) > 150 && $title == trim(substr($fullText, 0, 150))) {
             $title .= '...';
        }
    }
}
```

Regras de partição (nesta ordem):

1. Se existe `\n\n` → tudo antes é o título, tudo a partir do separador é o corpo
   (**o corpo preserva o `\n\n` inicial**, pois só há `trim()` nas pontas).
2. Senão, se há um `.` antes da posição 150 → título = até o ponto (inclusive);
   corpo = o restante.
3. Senão → título = 150 **bytes**; corpo = a partir do byte 150; e o título recebe
   `...` se o texto era maior que 150.

> **Bug (bytes × caracteres):** `substr`, `strlen` e `strpos` operam em **bytes**, não em
> caracteres UTF-8. Com texto em português acentuado, o corte de 150 bytes pode partir um
> caractere multibyte ao meio, produzindo **string UTF-8 inválida** no título. O mesmo
> vale para a comparação `$firstSentenceEnd < 150`. O correto seria `mb_substr`/`mb_strlen`.

> **Bug (corpo com separador):** no caso 1 o corpo devolvido começa com `\n\n`, o que
> faz o Summernote exibir um parágrafo vazio inicial.

---

## 4. Form Request — `GeminiPromptRequest`

Arquivo: `v5/app/Http/Requests/GeminiPromptRequest.php`

```php
namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;

class GeminiPromptRequest extends FormRequest
{
    public function authorize()
    {
        return true;
    }

    public function rules()
    {
        return [
            'prompt' => ['required', 'string', 'min:4', 'max:5000']
        ];
    }
}
```

| Campo | Regras | Efeito observável |
|---|---|---|
| `prompt` | `required` | ausente/ vazio → **422** |
| `prompt` | `string` | array/objeto → **422** |
| `prompt` | `min:4` | menos de 4 caracteres → **422** |
| `prompt` | `max:5000` | mais de 5000 caracteres → **422** |

Não há `prepareForValidation()`, `withValidator()`, `messages()` nem `attributes()` — o
Laravel usa as mensagens padrão em português do pacote
`laravellegends/pt-br-validator` (`v5/composer.json`).

`authorize()` retorna `true` **incondicionalmente** — a autorização depende
exclusivamente do middleware da rota (que não existe).

---

## 5. Formato de saída

Não há API Resource / Presenter. A resposta é um `response()->json()` artesanal.

### 5.1 Sucesso (HTTP 200)

```json
{
  "title": "Título gerado",
  "text": "Corpo do texto gerado"
}
```

### 5.2 Erro de credencial (HTTP 500)

```json
{
  "title": "Erro de Configuração",
  "text": "A chave da API do Google Gemini não está configurada ou foi lida incorretamente."
}
```

### 5.3 Resposta inesperada da API (HTTP 500)

```json
{
  "title": "Erro",
  "text": "Resposta inesperada da API Gemini."
}
```

### 5.4 Erro de comunicação (HTTP = status do upstream, ou 500)

```json
{
  "title": "Erro na Comunicação com a API Gemini",
  "text": "<mensagem crua do Guzzle> - Status: <status>"
}
```

> **Risco de vazamento:** a mensagem crua do Guzzle (que inclui a URL, e portanto a
> **chave da API na query string**) é devolvida ao cliente. Em caso de erro HTTP, o
> corpo da resposta do Gemini pode ser embutido na exceção e exposto.

### 5.5 Erro de validação (HTTP 422)

Formato padrão do Laravel:

```json
{
  "message": "The given data was invalid.",
  "errors": { "prompt": ["..."] }
}
```

> O `ia-modal.js` trata corretamente o 422 e mostra as mensagens de `errors`.

---

## 6. controllers do portal que consomem o caminho B (sem rota)

| Controller | Linha | Chamada |
|---|---|---|
| `v5/app/Http/Controllers/Portal/InformationController.php` | 112 | `'mask_request' => $this->MaskRequestWithGeminiAi($informationRequest['request'] ?? '')` |
| `v5/app/Http/Controllers/Portal/InformationController.php` | 320 | idem (fluxo Ajax) |
| `v5/app/Http/Controllers/Portal/InformationController.php` | 388-391 | `MaskRequestWithGeminiAi($requestText)` → `return $this->maskTextWithAi($requestText);` |
| `v5/app/Http/Controllers/Portal/OmbudsmanController.php` | 180 | `$ombudsman->mask_description = $this->maskTextWithAi($request->description ?? '');` |

Os dois controllers `use DataMaskTrait;` (`InformationController.php:26`,
`OmbudsmanController.php:41`).

> **Acoplamento:** `maskTextWithAi()` é chamado **dentro** do `store` do pedido, ou seja,
> **no ciclo da requisição HTTP**. Uma chamada lenta à API do Gemini (até 10 s) atrasa a
> resposta ao cidadão que acabou de enviar um pedido. Não há fila.

---

## 7. Permissões exigidas

| Recurso | Permissão |
|---|---|
| Rota `gemini.ia` | **nenhuma** (rota pública) |
| Tela de Configurações (CMS) | `v5/routes/basepro.php:161` → `Route::resource('configuracoes', 'Prefecture\ConfigurationController')->middleware('check.logged')` |
| Listagem de configurações | `has_access(['outros.index'])` (`ConfigurationController::index`) |
| Edição por categoria | `has_access(['configuracoes.edit'])` (`ConfigurationController::updateByCategory`) |
| Criação de configuração | exige `Sentinel::inRole('super-admin')` (`ConfigurationController::create`) |

> A rota `configuracoes/editar/{slug}` (`v5/routes/basepro.php:213`) tem **apenas**
> `check.logged` — sem `check.route.permission`. A checagem de papel fica dentro do
> controller. O mesmo vale para `update()`, que **não** chama `has_access()` antes de
> gravar: qualquer usuário logado que alcance o id da configuração pode alterá-la —
> inclusive o valor da chave da API.

---

## 8. Referências

- `v5/routes/web.php:5, 202-203, 213`
- `v5/routes/basepro.php:161, 213`
- `v5/app/Http/Kernel.php:30-44`
- `v5/app/Providers/RouteServiceProvider.php:54-56`
- `v5/app/Http/Middleware/CheckSiteManutention.php`
- `v5/app/Http/Middleware/CheckLogged.php`
- `v5/app/Http/Middleware/CheckRoutePermission.php`
- `v5/app/Http/Controllers/Prefecture/GeminiController.php:11-89`
- `v5/app/Http/Requests/GeminiPromptRequest.php`
- `v5/app/Http/Controllers/Portal/InformationController.php:112, 320, 388-391`
- `v5/app/Http/Controllers/Portal/OmbudsmanController.php:41, 180`
