docs: reescreve CLAUDE.md para refletir a arquitetura Nuxt real
A versao anterior descrevia uma SPA Vue com vue-router/main.js. O projeto e Nuxt 3 de verdade: roteamento por arquivo em src/pages/, autenticacao via padrao Token-Handler/BFF (token nunca chega ao browser, sessao em Redis) em vez do PKCE client-side "pendente" descrito antes, chamadas via $fetch + server/api/proxy (nao existe apiClient/apiClientPublico), variaveis NUXT_* (nao VITE_*). A tabela "O Que Esta Pendente" foi removida: credenciamento, primeiro acesso, certidao, IPTU e avisos ja estao implementados - verificado lendo os services e paginas reais. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
e4c468e61e
commit
fbb765db8d
364
CLAUDE.md
364
CLAUDE.md
@ -1,11 +1,14 @@
|
|||||||
# portal-modumfiscal-web — Contexto para IA
|
# portal-modumfiscal-web — Contexto para IA
|
||||||
|
|
||||||
|
> **Revisado por completo em 2026-09-25.** A versão anterior deste arquivo descrevia o projeto como uma SPA Vue com `vue-router`/`main.js` — isso está **errado**: o projeto é um app **Nuxt 3** real (SSR, roteamento por arquivo, BFF de autenticação). Praticamente todo o conteúdo sobre arquitetura, rotas e autenticação foi reescrito após leitura direta do código (`nuxt.config.ts`, `src/pages/`, `server/`, `src/plugins/`, `src/middleware/`). A tabela "O Que Está Pendente" da versão anterior também estava obsoleta — os itens que ela listava como pendentes (PKCE, certidão, IPTU, avisos, credenciamento, primeiro acesso) já estão implementados.
|
||||||
|
|
||||||
Portal público de autoatendimento fiscal do contribuinte. SaaS multi-tenant: cada
|
Portal público de autoatendimento fiscal do contribuinte. SaaS multi-tenant: cada
|
||||||
prefeitura cliente tem seu próprio subdomínio (`tutoia.modumfiscal.com.br`) e uma
|
prefeitura cliente tem seu próprio subdomínio (`tutoia.modumfiscal.com.br`) e uma
|
||||||
identidade visual distinta (cor primária + foto de fundo + logo).
|
identidade visual distinta (cor primária + foto de fundo + logo).
|
||||||
|
|
||||||
Projeto irmão: `core-modumfiscal-web` (backoffice interno da prefeitura, outra repo).
|
Projeto irmão: `core-modumfiscal-web` (backoffice interno da prefeitura, outro repo).
|
||||||
Backend compartilhado: API REST Spring Boot em `https://sistema.modumfiscal.com.br`.
|
Backend compartilhado: API REST Spring Boot em `core-modumfiscal-api`, sempre acessada
|
||||||
|
através do próprio servidor Nuxt (nunca direto do browser — ver seção de Autenticação).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@ -13,54 +16,60 @@ Backend compartilhado: API REST Spring Boot em `https://sistema.modumfiscal.com.
|
|||||||
|
|
||||||
| Dependência | Versão | Observação |
|
| Dependência | Versão | Observação |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
|
| Nuxt | 3.14 | `ssr: true`, `srcDir: 'src'`, `serverDir: 'server'` |
|
||||||
| Vue | 3.5 | `<script setup>` obrigatório |
|
| Vue | 3.5 | `<script setup>` obrigatório |
|
||||||
| Vite | 8 | Plugin `@tailwindcss/vite` (sem PostCSS) |
|
| Vite | 8 (via Nuxt) | Plugin `@tailwindcss/vite` (sem PostCSS) |
|
||||||
| PrimeVue | 4.5 | Tema Aura, auto-importado via `unplugin-vue-components` |
|
| PrimeVue | 4.5 | Módulo `@primevue/nuxt-module`, tema Aura, auto-importado |
|
||||||
| `@primeuix/themes` | 2 | `updatePreset`, `updateSurfacePalette`, `definePreset` |
|
| `@primeuix/themes` | 2 | `updatePreset`, `updateSurfacePalette`, `definePreset` |
|
||||||
| TailwindCSS | 4 | `@import "tailwindcss"` no CSS — **sem `tailwind.config.js`** |
|
| TailwindCSS | 4 | `@import "tailwindcss"` no CSS — **sem `tailwind.config.js`** |
|
||||||
| Vue Router | 5 | `createWebHistory` |
|
| Pinia | 3 | Módulos `@pinia/nuxt` + `pinia-plugin-persistedstate/nuxt` |
|
||||||
| Pinia | 3 | `pinia-plugin-persistedstate` |
|
| Axios | não usado | Chamadas HTTP são via `$fetch` nativo do Nuxt (ver Services) |
|
||||||
| Axios | 1.x | Dois clientes: público e autenticado |
|
| Zod | 4 | Validação de formulários (wizards) |
|
||||||
| Zod | 4 | Validação de formulários (usar nos wizards) |
|
| `ioredis` | 5 | Sessão + PKCE state (servidor apenas — ver Autenticação) |
|
||||||
|
| `jose` / `jwt-decode` | — | Decodificação de token no servidor |
|
||||||
|
| TypeScript | 5.6 | Usado em `server/` e em parte de `src/` (`.ts`); componentes `.vue` continuam em JS |
|
||||||
|
|
||||||
|
**Não existe roteamento client-side manual** (`vue-router` não é dependência direta do projeto) — o roteamento é **por arquivo**, via `src/pages/` (recurso nativo do Nuxt).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Arquitetura Multi-Tenant
|
## Arquitetura Multi-Tenant
|
||||||
|
|
||||||
```
|
```
|
||||||
subdomínio (ex: tutoia.modumfiscal.com.br)
|
requisição chega no servidor Nuxt (SSR)
|
||||||
└── getTenant() → 'tutoia'
|
└── plugins/prefeitura.server.ts (roda em toda request no servidor)
|
||||||
└── bootstrapPrefeitura() → GET /api/v1/publico/prefeitura/tutoia
|
└── tenantFromHost(host) → dominio (ex.: 'tutoia')
|
||||||
└── prefeituraStore.$patch({ nomePrefeitura, template, pathLogo, ... })
|
└── GET {coreApiUrl}/api/v1/publico/prefeitura/{dominio}
|
||||||
└── App.vue onMounted → applyTemplate('tutoia')
|
└── prefeituraStore.$patch({ nomePrefeitura, template, pathLogo, pathBackground, ... })
|
||||||
└── PrimeVue recebe cor primária laranja (tutoia)
|
|
||||||
|
└── plugins/auth.server.ts (roda em toda request no servidor)
|
||||||
|
└── GET /api/auth/me (usando o cookie de sessão da própria requisição)
|
||||||
|
└── authStore.setUser(...) — hidrata o estado autenticado ANTES do HTML ser enviado
|
||||||
|
|
||||||
|
app.vue (client, onMounted)
|
||||||
|
└── applyTemplate(prefeitura.template) + applySurface('slate')
|
||||||
|
→ PrimeVue já está inicializado neste ponto — pode aplicar o preset dinâmico
|
||||||
```
|
```
|
||||||
|
|
||||||
**Ordem de inicialização em `main.js` (crítica — não alterar):**
|
Os dois plugins (`prefeitura.server.ts`, `auth.server.ts`) rodam **no servidor, a cada requisição** — é assim que o SSR já entrega HTML com o tema e o estado de login corretos, sem flash de conteúdo errado. Não existe um `main.js`/bootstrap manual — a ordem de inicialização é controlada pelo próprio Nuxt (plugins → `app.vue` → hidratação client-side).
|
||||||
1. `createPinia()` + `pinia.use(persistedstate)`
|
|
||||||
2. `await bootstrapPrefeitura(pinia)` — busca config da prefeitura antes de montar
|
|
||||||
3. `app.use(router)`
|
|
||||||
4. `app.use(PrimeVue, primeVueConfig)`
|
|
||||||
5. `app.mount('#app')`
|
|
||||||
6. `App.vue onMounted` → `applyTemplate(prefeitura.template)` — aplica cor dinâmica
|
|
||||||
|
|
||||||
> **Por que `applyTemplate` fica no `onMounted` e não no bootstrap?**
|
> **Por que `applyTemplate` fica no `onMounted` de `app.vue` e não num plugin server-side?**
|
||||||
> `updatePreset()` exige que o PrimeVue já esteja inicializado. No bootstrap,
|
> `updatePreset()` do PrimeVue precisa rodar no client, depois que o PrimeVue já foi montado — chamá-la no servidor não tem efeito sobre o CSS enviado ao browser.
|
||||||
> o `app.use(PrimeVue)` ainda não rodou. Chamá-la antes causa erro silencioso.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Detecção de Tenant
|
## Detecção de Tenant
|
||||||
|
|
||||||
```js
|
Existe em **duas cópias equivalentes** (uma para o server, uma para uso client-side), ambas em TypeScript:
|
||||||
// src/utils/tenant.js
|
- `server/utils/tenant.ts` — usado pelos plugins/rotas de servidor (`tenantFromEvent`, a partir do header `Host`)
|
||||||
getTenant() → hostname.split('.')[0]
|
- `src/utils/tenant.ts` — usado no client quando necessário
|
||||||
→ localStorage('current_dominio') // fallback
|
|
||||||
→ 'sistema' // último fallback
|
|
||||||
```
|
|
||||||
|
|
||||||
`localhost` é inválido propositalmente — em dev, o bootstrap falha graciosamente
|
```ts
|
||||||
e o app sobe sem tema personalizado (usa `blue` como padrão).
|
tenantFromHost(host) → primeiro subdomínio, minúsculo
|
||||||
|
→ lista de bloqueio (www, api, admin, test, dev, staging, production, localhost, ...) cai no fallback
|
||||||
|
→ subdomínio inválido (não bate `/^[a-z0-9-]+$/`) cai no fallback
|
||||||
|
→ fallback final: 'sistema'
|
||||||
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@ -74,7 +83,7 @@ A superfície é sempre `slate` — não mudar.
|
|||||||
|
|
||||||
Para adicionar novo município com foto de fundo:
|
Para adicionar novo município com foto de fundo:
|
||||||
1. Colocar a imagem em `src/assets/images/bg-NOME.jpeg`
|
1. Colocar a imagem em `src/assets/images/bg-NOME.jpeg`
|
||||||
2. Importar em `HomeView.vue` e adicionar ao `heroBgMap`
|
2. Importar em `HomeView`/página correspondente e adicionar ao mapa de fundos
|
||||||
3. Cadastrar o `template` correspondente em `theme.config.js` se precisar de cor nova
|
3. Cadastrar o `template` correspondente em `theme.config.js` se precisar de cor nova
|
||||||
|
|
||||||
---
|
---
|
||||||
@ -101,18 +110,59 @@ Para adicionar novo município com foto de fundo:
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Clientes HTTP
|
## Autenticação — padrão Token-Handler (BFF), não PKCE client-side
|
||||||
|
|
||||||
```js
|
**O token de acesso nunca chega ao browser.** Toda a troca OAuth2/PKCE com o Keycloak acontece no servidor Nuxt; o cliente só recebe um cookie de sessão **httpOnly** opaco.
|
||||||
// src/config/apiClient.js
|
|
||||||
apiClientPublico // sem Authorization — para bootstrap e serviços públicos
|
```
|
||||||
apiClient // com Bearer token + headers tenant — para /portal/*
|
1. Usuário clica "Entrar" → abre LoginModal.vue (composable useLoginModal)
|
||||||
|
2. POST /api/auth/login { documento?, returnTo? }
|
||||||
|
server/api/auth/login.post.ts:
|
||||||
|
gera PKCE (code_verifier/code_challenge) + state
|
||||||
|
salva o par em Redis (server/utils/pkce.ts, TTL configurável)
|
||||||
|
devolve { authUrl } apontando pro Keycloak
|
||||||
|
3. Browser navega para authUrl (Keycloak)
|
||||||
|
4. Keycloak redireciona pra GET /api/auth/callback?code=...&state=...
|
||||||
|
server/api/auth/callback.get.ts:
|
||||||
|
recupera o PKCE state salvo (consumePkceState)
|
||||||
|
troca o code por tokens (server-side, com client secret)
|
||||||
|
cria sessão em Redis (server/utils/session.ts) com access/refresh/id token
|
||||||
|
seta cookie de sessão httpOnly (setSessionCookie)
|
||||||
|
redireciona pro returnTo original (padrão: /portal/painel)
|
||||||
|
5. Toda página/plugin subsequente:
|
||||||
|
GET /api/auth/me → lê sessão do cookie → devolve só nome/documento/email/roles
|
||||||
|
authStore (Pinia) guarda SÓ esses metadados — nunca o token
|
||||||
```
|
```
|
||||||
|
|
||||||
Interceptors adicionam automaticamente `X-Municipio` e `X-Dominio` em toda requisição.
|
- **Middleware de rota** (`src/middleware/auth.ts`): protege `/portal/**`. Se não autenticado, **abre o modal global de login** (não redireciona pra uma página `/login` — essa página não existe mais como fluxo principal; `pages/entrar.vue` hoje é só um stub que redireciona pra `/` ou `/portal/painel`).
|
||||||
`apiClient` redireciona para `/` em resposta 401.
|
- **Refresh de token**: transparente, feito pelo proxy (`server/api/proxy/[...path].ts`) via `getValidAccessToken(sid)` — a página nunca lida com expiração de token diretamente.
|
||||||
|
- **Logout**: `POST /api/auth/logout` — invalida a sessão em Redis e limpa o cookie.
|
||||||
|
|
||||||
**Regra:** nunca chamar `apiClient` diretamente em componente — sempre via Service.
|
---
|
||||||
|
|
||||||
|
## Chamada à API — sempre via `/api/proxy/**`
|
||||||
|
|
||||||
|
**Não existe `apiClient.js`/`apiClientPublico`.** Os services usam o `$fetch` global do Nuxt, sempre contra o proxy do próprio servidor:
|
||||||
|
|
||||||
|
```js
|
||||||
|
// src/services/[dominio]Service.js
|
||||||
|
const FETCH_HEADERS = { 'X-Requested-With': 'fetch' }
|
||||||
|
function proxyUrl(path) { return `/api/proxy${path}` }
|
||||||
|
|
||||||
|
export const meuService = {
|
||||||
|
listar(params) {
|
||||||
|
return $fetch(proxyUrl('/contribuinte/algo'), { headers: FETCH_HEADERS, query: params })
|
||||||
|
},
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`server/api/proxy/[...path].ts` é o único ponto que fala com `core-modumfiscal-api`:
|
||||||
|
- Lê a sessão do cookie e busca o access token válido em Redis (refresh automático se expirado)
|
||||||
|
- Resolve o tenant pelo host e busca `codigoMunicipio` (cacheado em Redis)
|
||||||
|
- Encaminha a chamada com `Authorization: Bearer ...` + `X-Municipio` + `X-Dominio` injetados
|
||||||
|
- Rotas sob `/api/v1/publico/**` passam **sem exigir sessão** (`isPublico` no proxy)
|
||||||
|
|
||||||
|
**Regra:** nunca chamar `$fetch` direto pra URL do core-api num componente/página — sempre via service, sempre via `/api/proxy/...`.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@ -120,115 +170,150 @@ Interceptors adicionam automaticamente `X-Municipio` e `X-Dominio` em toda requi
|
|||||||
|
|
||||||
```
|
```
|
||||||
src/
|
src/
|
||||||
|
├── app.vue ← splash de boot + NuxtLayout/NuxtPage + Toast + LoginModal global
|
||||||
├── assets/
|
├── assets/
|
||||||
│ ├── main.css ← Tailwind v4 directives + @variant dark
|
│ ├── main.css Tailwind v4 directives + @variant dark
|
||||||
│ ├── layout/layout.scss ← Focus ring, skip link, prefers-reduced-motion, classes a11y
|
│ ├── layout/layout.scss Focus ring, skip link, prefers-reduced-motion, classes a11y
|
||||||
│ └── images/ ← Logos e fotos de fundo (importação estática Vite)
|
│ └── images/ Logos e fotos de fundo (importação estática Vite)
|
||||||
├── bootstrap/
|
|
||||||
│ └── prefeituraBoot.js ← Busca config da prefeitura antes de montar o app
|
|
||||||
├── components/
|
├── components/
|
||||||
│ ├── common/
|
│ ├── common/ AppHeader, AppFooter, ServiceCard, AccessibilityWidget
|
||||||
│ │ ├── AppHeader.vue ← Header público (logo dinâmica, nav)
|
│ └── auth/ DocumentoInput.vue, LoginModal.vue
|
||||||
│ │ ├── AppFooter.vue ← Footer com links institucionais
|
|
||||||
│ │ ├── ServiceCard.vue ← Card reutilizável de serviço (público e autenticado)
|
|
||||||
│ │ └── AccessibilityWidget.vue ← Botão flutuante: fonte, dark, alto contraste
|
|
||||||
│ └── auth/
|
|
||||||
│ └── DocumentoInput.vue ← Input CPF/CNPJ com máscara automática
|
|
||||||
├── composables/
|
├── composables/
|
||||||
│ └── useMotion.js ← prefers-reduced-motion reativo
|
│ ├── useMotion.js prefers-reduced-motion reativo
|
||||||
|
│ ├── useAuth.ts wrapper de authStore + chamadas de login/logout
|
||||||
|
│ └── useLoginModal.ts abre/fecha o LoginModal global, guarda o returnTo
|
||||||
├── config/
|
├── config/
|
||||||
│ ├── apiClient.js ← Axios clients (público + autenticado)
|
│ ├── primevue.config.js Aura preset + locale pt-BR
|
||||||
│ ├── primevue.config.js ← Aura preset + locale pt-BR
|
│ └── theme.config.js Paletas e applyTemplate()/applySurface()
|
||||||
│ └── theme.config.js ← Paletas e applyTemplate()
|
|
||||||
├── layouts/
|
├── layouts/
|
||||||
│ ├── PublicLayout.vue ← Header + Footer + RouterView (rotas públicas)
|
│ ├── default.vue Header + Footer + RouterView (rotas públicas — NÃO se chama PublicLayout)
|
||||||
│ └── PortalLayout.vue ← Header autenticado com nav + Sair
|
│ └── portal.vue Header autenticado com nav + Sair (NÃO se chama PortalLayout)
|
||||||
├── router/index.js ← Rotas + guard requiresAuth
|
├── middleware/
|
||||||
├── services/
|
│ └── auth.ts Guard de `/portal/**` — abre o LoginModal (ver Autenticação)
|
||||||
│ ├── prefeituraService.js ← GET /publico/prefeitura/{dominio}
|
├── pages/ ← ROTEAMENTO POR ARQUIVO (Nuxt) — não existe router/index.js
|
||||||
│ └── authService.js ← Keycloak PKCE (estrutura pronta, integração pendente)
|
│ ├── index.vue → /
|
||||||
|
│ ├── entrar.vue → /entrar (stub de redirect — login real é via modal)
|
||||||
|
│ ├── credenciamento.vue → /credenciamento (wizard, ~6 etapas)
|
||||||
|
│ ├── primeiro-acesso.vue → /primeiro-acesso (wizard, 3 etapas)
|
||||||
|
│ ├── servicos/
|
||||||
|
│ │ ├── index.vue → /servicos
|
||||||
|
│ │ ├── certidao.vue → /servicos/certidao
|
||||||
|
│ │ └── iptu.vue → /servicos/iptu
|
||||||
|
│ └── portal/
|
||||||
|
│ ├── painel.vue → /portal/painel
|
||||||
|
│ ├── debitos.vue → /portal/debitos
|
||||||
|
│ ├── pagamentos.vue → /portal/pagamentos
|
||||||
|
│ ├── guias.vue → /portal/guias
|
||||||
|
│ ├── certidoes.vue → /portal/certidoes
|
||||||
|
│ ├── dados.vue → /portal/dados
|
||||||
|
│ └── taxas/
|
||||||
|
│ ├── index.vue → /portal/taxas
|
||||||
|
│ └── emitir.vue → /portal/taxas/emitir
|
||||||
|
├── plugins/
|
||||||
|
│ ├── prefeitura.server.ts Bootstrap do tenant — roda a cada request no servidor
|
||||||
|
│ └── auth.server.ts Hidrata authStore via /api/auth/me — roda a cada request no servidor
|
||||||
|
├── services/ avisoService, certidaoService, credenciamentoService,
|
||||||
|
│ iptuService, portalService, primeiroAcessoService — todos via $fetch + /api/proxy
|
||||||
├── stores/
|
├── stores/
|
||||||
│ ├── prefeituraStore.js ← Config da prefeitura (persiste localStorage)
|
│ ├── prefeituraStore.js Config da prefeitura (persiste localStorage; Options API por legado)
|
||||||
│ └── authStore.js ← Token + userInfo (persiste localStorage)
|
│ └── authStore.js SÓ metadados do usuário — token nunca fica aqui nem no cliente
|
||||||
├── utils/
|
└── utils/
|
||||||
│ └── tenant.js ← getTenant / setTenant / clearTenant
|
└── tenant.ts Espelho client-side de server/utils/tenant.ts
|
||||||
└── views/
|
|
||||||
├── public/ ← Home, Login, PrimeiroAcesso, Credenciamento
|
server/
|
||||||
├── servicos/ ← ServicosHub, Certidao, Iptu
|
├── api/
|
||||||
└── portal/ ← Painel, Debitos, Certidoes, Alvaras, Pagamentos, Dados
|
│ ├── auth/ login.post.ts, callback.get.ts, me.get.ts, refresh.post.ts, logout.post.ts
|
||||||
|
│ └── proxy/[...path].ts Único gateway para o core-api (ver seção acima)
|
||||||
|
├── middleware/
|
||||||
|
│ └── csrf.ts
|
||||||
|
└── utils/
|
||||||
|
├── keycloak.ts buildAuthUrl, exchangeCodeForTokens, userInfoFromAccessToken
|
||||||
|
├── pkce.ts generatePkce, savePkceState, consumePkceState (Redis)
|
||||||
|
├── session.ts createSession, getValidAccessToken, setSessionCookie, etc. (Redis)
|
||||||
|
├── redis.ts cliente ioredis compartilhado
|
||||||
|
├── prefeitura.ts fetchPrefeituraInfo (cacheado em Redis)
|
||||||
|
└── tenant.ts tenantFromHost / tenantFromEvent
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Rotas
|
## Rotas
|
||||||
|
|
||||||
| Path | Nome | Layout | Auth |
|
Roteamento **por arquivo** — criar um `.vue` em `src/pages/` já cria a rota, sem passo de registro manual.
|
||||||
|---|---|---|---|
|
|
||||||
| `/` | `home` | Public | ✗ |
|
|
||||||
| `/login` | `login` | Public | ✗ |
|
|
||||||
| `/primeiro-acesso` | `primeiro-acesso` | Public | ✗ |
|
|
||||||
| `/credenciamento` | `credenciamento` | Public | ✗ |
|
|
||||||
| `/servicos` | `servicos` | Public | ✗ |
|
|
||||||
| `/servicos/certidao` | `certidao` | Public | ✗ |
|
|
||||||
| `/servicos/iptu` | `iptu` | Public | ✗ |
|
|
||||||
| `/portal/painel` | `painel` | Portal | ✓ |
|
|
||||||
| `/portal/debitos` | `debitos` | Portal | ✓ |
|
|
||||||
| `/portal/certidoes` | `certidoes-portal` | Portal | ✓ |
|
|
||||||
| `/portal/alvaras` | `alvaras` | Portal | ✓ |
|
|
||||||
| `/portal/pagamentos` | `pagamentos` | Portal | ✓ |
|
|
||||||
| `/portal/dados` | `dados` | Portal | ✓ |
|
|
||||||
|
|
||||||
Guard no router: rotas `requiresAuth: true` redirecionam para `home` se não autenticado.
|
| Path | Arquivo | Layout | Auth |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `/` | `pages/index.vue` | `default` | ✗ |
|
||||||
|
| `/entrar` | `pages/entrar.vue` | — | stub de redirect |
|
||||||
|
| `/credenciamento` | `pages/credenciamento.vue` | `default` | ✗ |
|
||||||
|
| `/primeiro-acesso` | `pages/primeiro-acesso.vue` | `default` | ✗ |
|
||||||
|
| `/servicos` | `pages/servicos/index.vue` | `default` | ✗ |
|
||||||
|
| `/servicos/certidao` | `pages/servicos/certidao.vue` | `default` | ✗ |
|
||||||
|
| `/servicos/iptu` | `pages/servicos/iptu.vue` | `default` | ✗ |
|
||||||
|
| `/portal/painel` | `pages/portal/painel.vue` | `portal` | ✓ (`middleware/auth.ts`) |
|
||||||
|
| `/portal/debitos` | `pages/portal/debitos.vue` | `portal` | ✓ |
|
||||||
|
| `/portal/pagamentos` | `pages/portal/pagamentos.vue` | `portal` | ✓ |
|
||||||
|
| `/portal/guias` | `pages/portal/guias.vue` | `portal` | ✓ |
|
||||||
|
| `/portal/certidoes` | `pages/portal/certidoes.vue` | `portal` | ✓ |
|
||||||
|
| `/portal/dados` | `pages/portal/dados.vue` | `portal` | ✓ |
|
||||||
|
| `/portal/taxas` | `pages/portal/taxas/index.vue` | `portal` | ✓ |
|
||||||
|
| `/portal/taxas/emitir` | `pages/portal/taxas/emitir.vue` | `portal` | ✓ |
|
||||||
|
|
||||||
|
> Não existe mais `/portal/alvaras` — o módulo de Alvará foi removido do produto (consolidado em Taxas), consistente com o `core-modumfiscal-api`.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Fluxo de Login (2 etapas)
|
## Fluxo de Login (modal, não página)
|
||||||
|
|
||||||
```
|
```
|
||||||
Home (captura CPF/CNPJ)
|
Qualquer página → clique em "Entrar" ou navegação bloqueada por middleware/auth.ts
|
||||||
└── router.push({ name: 'login', query: { doc: documento } })
|
└── useLoginModal().open(returnTo) → abre LoginModal.vue (renderizado globalmente em app.vue)
|
||||||
└── LoginView.vue exibe o doc mascarado + campo senha
|
└── LoginModal chama POST /api/auth/login → recebe { authUrl } → window.location = authUrl
|
||||||
└── entrar() → Keycloak PKCE (a implementar em authService.js)
|
└── Keycloak autentica → GET /api/auth/callback → sessão criada → redirect pro returnTo
|
||||||
└── authStore.setSession(token, userInfo)
|
|
||||||
└── router.push({ name: 'painel' })
|
|
||||||
```
|
```
|
||||||
|
|
||||||
O documento trafega como query param (`?doc=01234567890`) — apenas os dígitos,
|
Não existe mais uma página `LoginView.vue` com captura de CPF/CNPJ em duas etapas — isso foi substituído pelo modal global. `pages/entrar.vue` existe só como redirecionamento de compatibilidade.
|
||||||
sem formatação. `LoginView` faz a máscara localmente.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Variáveis de Ambiente
|
## Variáveis de Ambiente
|
||||||
|
|
||||||
|
Nuxt usa `runtimeConfig` (não `VITE_*`). Chaves reais, definidas em `nuxt.config.ts` e sobrescritas via `.env` local ou secret manager em produção:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# .env.development.local (GIT IGNORADO — crie localmente)
|
# .env (GIT IGNORADO — copiar de .env.example e preencher)
|
||||||
VITE_API_URL=https://sistema.modumfiscal.com.br
|
NUXT_CORE_API_URL=... # URL do core-modumfiscal-api, usada só no servidor
|
||||||
VITE_KEYCLOAK_URL=https://keycloakprod.modumfiscal.com.br
|
NUXT_KEYCLOAK_URL=...
|
||||||
VITE_KEYCLOAK_REALM=modumfiscal-dev
|
NUXT_KEYCLOAK_REALM=...
|
||||||
VITE_KEYCLOAK_CLIENT_ID=portal-modumfiscal-web
|
NUXT_KEYCLOAK_CLIENT_ID=...
|
||||||
|
NUXT_KEYCLOAK_CLIENT_SECRET=... # client confidential — nunca vai pro bundle client-side
|
||||||
|
NUXT_REDIS_URL=... # sessão + PKCE state
|
||||||
|
NUXT_COOKIE_SECRET=... # assina o cookie de sessão — rotacionar em incidente
|
||||||
|
NUXT_SESSION_TTL_SECONDS=... # default 8h
|
||||||
|
NUXT_PKCE_TTL_SECONDS=... # default 5min
|
||||||
```
|
```
|
||||||
|
|
||||||
Os arquivos `.env.development` e `.env.production` estão no repo mas vazios —
|
Todas essas chaves são `runtimeConfig` **server-only** (não expostas ao client) — só `runtimeConfig.public.appName` é pública. `.env.production` no repo está vazio (template); valores reais só em `.env` local ou no secret manager do orquestrador.
|
||||||
servem de template. Os valores reais ficam apenas nos arquivos `.local`.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Regras Críticas de Código
|
## Regras Críticas de Código
|
||||||
|
|
||||||
- **`<script setup>` obrigatório** — Options API proibida (exceto `prefeituraStore` que usa Options API por compatibilidade com o padrão do core)
|
- **`<script setup>` obrigatório** — Options API proibida (exceto `prefeituraStore.js`, que usa Options API por compatibilidade com o padrão do core)
|
||||||
- **PrimeVue auto-importado** — não adicionar `import Button from 'primevue/button'` manualmente
|
- **PrimeVue auto-importado** — não adicionar `import Button from 'primevue/button'` manualmente
|
||||||
- **CSS:** TailwindCSS inline no template — `@apply` proibido — SCSS só em `assets/layout/`
|
- **CSS:** TailwindCSS inline no template — `@apply` proibido — SCSS só em `assets/layout/`
|
||||||
- **`dark:` em toda view nova** — ver tabela acima
|
- **`dark:` em toda view nova** — ver tabela acima
|
||||||
- **Máximo 300 linhas por componente** — extrair subcomponentes se ultrapassar
|
- **Máximo 300 linhas por página/componente** — extrair subcomponentes se ultrapassar
|
||||||
- **Nunca `apiClient` direto em componente** — sempre via Service
|
- **Nunca `$fetch` direto num componente/página apontando pro core-api** — sempre via service, sempre via `/api/proxy/...`
|
||||||
- **`router.push({ name: 'home' })` para "voltar"** — nunca `router.back()` em fluxos críticos (histórico inesperado)
|
- **Nunca guardar token no client** (localStorage, cookie não-httpOnly, store) — o padrão é Token-Handler/BFF; se uma feature nova parecer exigir isso, é sinal de que deveria virar mais uma rota em `server/api/`
|
||||||
- **Componentes PrimeVue com `dark:` manual nos wrappers** — PrimeVue já adapta seus internos, mas divs externas precisam de dark: Tailwind
|
- **`router.push({ name: 'index' })` (ou `navigateTo('/')`) para "voltar"** — nunca `router.back()` em fluxos críticos
|
||||||
|
- **Componentes PrimeVue com `dark:` manual nos wrappers** — PrimeVue já adapta seus internos, mas divs externas precisam de `dark:` Tailwind
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Acessibilidade
|
## Acessibilidade
|
||||||
|
|
||||||
Widget flutuante (`AccessibilityWidget.vue`) disponível em todas as telas via `App.vue`.
|
Widget flutuante (`AccessibilityWidget.vue`) disponível em todas as telas via `app.vue`.
|
||||||
Persiste preferências no `localStorage` (chaves: `a11y-fonte`, `a11y-contraste`, `a11y-escuro`).
|
Persiste preferências no `localStorage` (chaves: `a11y-fonte`, `a11y-contraste`, `a11y-escuro`).
|
||||||
|
|
||||||
Classes aplicadas no `<html>`:
|
Classes aplicadas no `<html>`:
|
||||||
@ -244,43 +329,30 @@ ARIA implementada: skip link, `aria-label`, `aria-current`, `role="alert"`, `ari
|
|||||||
|
|
||||||
## Como Adicionar Nova Tela Pública
|
## Como Adicionar Nova Tela Pública
|
||||||
|
|
||||||
1. Criar `src/views/servicos/MinhaView.vue`
|
1. Criar `src/pages/servicos/minha-tela.vue` (o arquivo **já é a rota** — nada para registrar)
|
||||||
2. Adicionar rota em `src/router/index.js` dentro do bloco `PublicLayout`
|
2. Definir o layout se não for o `default` (`definePageMeta({ layout: '...' })`)
|
||||||
3. Incluir botão voltar (`router.push({ name: 'home' })`) no topo
|
3. Incluir botão voltar (`navigateTo('/')`) no topo
|
||||||
4. Adicionar `dark:` em todos os utilitários Tailwind
|
4. Adicionar `dark:` em todos os utilitários Tailwind
|
||||||
5. (Opcional) Adicionar no `servicosPublicos` de `HomeView.vue`
|
5. Criar/usar um service em `src/services/` que chame `$fetch` via `/api/proxy/...`
|
||||||
|
6. (Opcional) Adicionar no hub de serviços (`pages/servicos/index.vue`)
|
||||||
---
|
|
||||||
|
|
||||||
## Como Adicionar Nova Tela Autenticada
|
## Como Adicionar Nova Tela Autenticada
|
||||||
|
|
||||||
1. Criar `src/views/portal/MinhaView.vue`
|
1. Criar `src/pages/portal/minha-tela.vue`
|
||||||
2. Adicionar rota em `src/router/index.js` dentro do bloco `PortalLayout` com `meta: { requiresAuth: true }` já herdado do pai
|
2. `definePageMeta({ layout: 'portal' })` — o `middleware/auth.ts` protege tudo sob `/portal/**` automaticamente (confirmar se o middleware está registrado globalmente para o prefixo ou se precisa ser declarado por página antes de assumir)
|
||||||
3. Criar serviço em `src/services/` que use `apiClient` (nunca `apiClientPublico`)
|
3. Criar serviço em `src/services/` usando `$fetch` + `/api/proxy/...` (nunca rota pública)
|
||||||
4. Adicionar no `navItems` de `PortalLayout.vue` se entrar na navegação principal
|
4. Adicionar na navegação do `layouts/portal.vue` se entrar no menu principal
|
||||||
5. Adicionar `dark:` em todos os utilitários Tailwind
|
5. Adicionar `dark:` em todos os utilitários Tailwind
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## O Que Está Pendente
|
|
||||||
|
|
||||||
| Feature | Arquivos envolvidos | Observações |
|
|
||||||
|---|---|---|
|
|
||||||
| Keycloak PKCE real | `authService.js`, `LoginView.vue` | Estrutura pronta, falta integrar `pkce-challenge` |
|
|
||||||
| Wizard Credenciamento | `CredenciamentoView.vue` | 6 etapas: dados pessoais, endereço, documentos, representante, confirmação |
|
|
||||||
| Wizard Primeiro Acesso | `PrimeiroAcessoView.vue` | 4 etapas: identificação, código, nova senha, confirmação |
|
|
||||||
| Certidão (implementação real) | `CertidaoView.vue` | Input CPF/CNPJ → POST API → download PDF |
|
|
||||||
| IPTU (implementação real) | `IptuView.vue` | Input inscrição imobiliária → GET débitos → carnê PDF |
|
|
||||||
| Endpoint avisos carousel | `HomeView.vue` | `GET /api/v1/publico/avisos/{dominio}` (dados mockados atualmente) |
|
|
||||||
| Campo `pathBackground` na API | `prefeituraBoot.js` | Backend ainda não retorna — mapeamento estático por `template` como POC |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Comandos
|
## Comandos
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm run dev # dev server (http://localhost:5173)
|
npm run dev # nuxt dev — http://localhost:3000 (ou porta configurada)
|
||||||
npm run build # build produção
|
npm run build # nuxt build
|
||||||
|
npm run generate # nuxt generate
|
||||||
|
npm run preview # nuxt preview
|
||||||
npm run lint # ESLint + fix
|
npm run lint # ESLint + fix
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -290,10 +362,20 @@ npm run lint # ESLint + fix
|
|||||||
|
|
||||||
| Erro | Correto |
|
| Erro | Correto |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `applyTemplate()` no bootstrap | Chamar no `App.vue onMounted` — PrimeVue precisa estar inicializado |
|
| Assumir SPA com `vue-router`/`main.js` | É Nuxt 3 — roteamento por arquivo em `src/pages/`, bootstrap via `plugins/*.server.ts` |
|
||||||
| `router.back()` no botão voltar | `router.push({ name: 'home' })` — back usa histórico do browser |
|
| `applyTemplate()` fora do `onMounted` de `app.vue` | PrimeVue precisa estar inicializado no client primeiro |
|
||||||
|
| Redirecionar pra uma página `/login` | Login é o `LoginModal.vue` global, via `useLoginModal()` |
|
||||||
|
| Guardar token de acesso no client | Padrão é Token-Handler — token fica só em Redis, servidor injeta via proxy |
|
||||||
| `import Button from 'primevue/button'` | Não importar — PrimeVue é auto-importado |
|
| `import Button from 'primevue/button'` | Não importar — PrimeVue é auto-importado |
|
||||||
| `@apply` no template | TailwindCSS inline — `@apply` só em `layout.scss` e nunca |
|
| `@apply` no template | TailwindCSS inline — `@apply` só em `layout.scss` |
|
||||||
| Utilitário Tailwind sem `dark:` | Sempre adicionar par dark: para qualquer bg/text/border |
|
| Utilitário Tailwind sem `dark:` | Sempre adicionar par `dark:` para qualquer bg/text/border |
|
||||||
| `apiClient` direto na view | Criar Service e chamar pela view |
|
| `$fetch` direto pra URL do core-api | Sempre via service, sempre via `/api/proxy/...` |
|
||||||
| Path relativo de logo/foto da API | `resolverUrl()` em `prefeituraBoot.js` converte para URL absoluta |
|
| Variável de ambiente `VITE_*` | Nuxt usa `runtimeConfig`, variáveis reais são `NUXT_*` |
|
||||||
|
| Path relativo de logo/foto da API | `resolverUrl()` em `plugins/prefeitura.server.ts` converte para URL absoluta |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Pontos não totalmente confirmados nesta revisão
|
||||||
|
|
||||||
|
- Se `pathBackground` já é populado de fato pelo `core-modumfiscal-api` (o código do plugin já lê o campo da resposta, mas não confirmei o DTO do backend).
|
||||||
|
- Mecanismo exato de como `middleware/auth.ts` é aplicado a todas as rotas `/portal/**` (globalmente vs. por página) — confirmar em `nuxt.config.ts`/cada página antes de assumir ao criar uma tela nova.
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user