diff --git a/.gitignore b/.gitignore index e458ed5..4018f95 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,9 @@ .worktrees/ + +# Secrets — NUNCA commitar +.env +.env.prod +.env.local +back-end/.env +back-end/.env.local +!back-end/.env.example diff --git a/.playwright-mcp/console-2026-04-02T18-39-05-247Z.log b/.playwright-mcp/console-2026-04-02T18-39-05-247Z.log new file mode 100644 index 0000000..e3381c7 --- /dev/null +++ b/.playwright-mcp/console-2026-04-02T18-39-05-247Z.log @@ -0,0 +1,23 @@ +[ 44110ms] [ERROR] Failed to load resource: the server responded with a status of 404 (Not Found) @ http://163.176.236.167/api/_nuxt_icon/lucide.json?icons=loader-circle:0 +[ 44111ms] [WARNING] [Icon] failed to load icon `lucide:loader-circle` @ http://163.176.236.167/_nuxt/CCWgtBsX.js:5 +[ 45020ms] [ERROR] Failed to load resource: the server responded with a status of 404 (Not Found) @ http://163.176.236.167/api/_nuxt_icon/heroicons.json?icons=arrow-right-on-rectangle%2Cbuilding-library%2Cbuilding-office-2%2Cchart-bar%2Cclipboard-document-list%2Cclock%2Ccog-6-tooth%2Cdocument-text%2Cfolder%2Chome%2Cmagnifying-glass%2Cmegaphone%2Cpencil-square%2Cplay%2Cscale%2Ctrophy%2Cusers%2Cview-columns%2Cx-circle:0 +[ 45021ms] [WARNING] [Icon] failed to load icon `heroicons:home` @ http://163.176.236.167/_nuxt/CCWgtBsX.js:5 +[ 45021ms] [WARNING] [Icon] failed to load icon `heroicons:home` @ http://163.176.236.167/_nuxt/CCWgtBsX.js:5 +[ 45021ms] [WARNING] [Icon] failed to load icon `heroicons:clipboard-document-list` @ http://163.176.236.167/_nuxt/CCWgtBsX.js:5 +[ 45021ms] [WARNING] [Icon] failed to load icon `heroicons:magnifying-glass` @ http://163.176.236.167/_nuxt/CCWgtBsX.js:5 +[ 45021ms] [WARNING] [Icon] failed to load icon `heroicons:pencil-square` @ http://163.176.236.167/_nuxt/CCWgtBsX.js:5 +[ 45021ms] [WARNING] [Icon] failed to load icon `heroicons:megaphone` @ http://163.176.236.167/_nuxt/CCWgtBsX.js:5 +[ 45021ms] [WARNING] [Icon] failed to load icon `heroicons:play` @ http://163.176.236.167/_nuxt/CCWgtBsX.js:5 +[ 45021ms] [WARNING] [Icon] failed to load icon `heroicons:scale` @ http://163.176.236.167/_nuxt/CCWgtBsX.js:5 +[ 45021ms] [WARNING] [Icon] failed to load icon `heroicons:trophy` @ http://163.176.236.167/_nuxt/CCWgtBsX.js:5 +[ 45021ms] [WARNING] [Icon] failed to load icon `heroicons:x-circle` @ http://163.176.236.167/_nuxt/CCWgtBsX.js:5 +[ 45021ms] [WARNING] [Icon] failed to load icon `heroicons:view-columns` @ http://163.176.236.167/_nuxt/CCWgtBsX.js:5 +[ 45021ms] [WARNING] [Icon] failed to load icon `heroicons:folder` @ http://163.176.236.167/_nuxt/CCWgtBsX.js:5 +[ 45021ms] [WARNING] [Icon] failed to load icon `heroicons:clock` @ http://163.176.236.167/_nuxt/CCWgtBsX.js:5 +[ 45021ms] [WARNING] [Icon] failed to load icon `heroicons:building-library` @ http://163.176.236.167/_nuxt/CCWgtBsX.js:5 +[ 45021ms] [WARNING] [Icon] failed to load icon `heroicons:building-office-2` @ http://163.176.236.167/_nuxt/CCWgtBsX.js:5 +[ 45021ms] [WARNING] [Icon] failed to load icon `heroicons:document-text` @ http://163.176.236.167/_nuxt/CCWgtBsX.js:5 +[ 45021ms] [WARNING] [Icon] failed to load icon `heroicons:chart-bar` @ http://163.176.236.167/_nuxt/CCWgtBsX.js:5 +[ 45021ms] [WARNING] [Icon] failed to load icon `heroicons:users` @ http://163.176.236.167/_nuxt/CCWgtBsX.js:5 +[ 45021ms] [WARNING] [Icon] failed to load icon `heroicons:cog-6-tooth` @ http://163.176.236.167/_nuxt/CCWgtBsX.js:5 +[ 45021ms] [WARNING] [Icon] failed to load icon `heroicons:arrow-right-on-rectangle` @ http://163.176.236.167/_nuxt/CCWgtBsX.js:5 diff --git a/.playwright-mcp/page-2026-04-02T18-39-06-804Z.yml b/.playwright-mcp/page-2026-04-02T18-39-06-804Z.yml new file mode 100644 index 0000000..be04548 --- /dev/null +++ b/.playwright-mcp/page-2026-04-02T18-39-06-804Z.yml @@ -0,0 +1,20 @@ +- generic [ref=e4]: + - generic [ref=e5]: + - heading "Bem-vindo de volta" [level=1] [ref=e6] + - paragraph [ref=e7]: Informe suas credenciais para acessar o sistema + - generic [ref=e8]: + - generic [ref=e9]: + - generic [ref=e12]: E-mail + - textbox "E-mail" [ref=e15]: + - /placeholder: usuario@orgao.gov.br + - generic [ref=e16]: + - generic [ref=e19]: Senha + - textbox "Senha" [ref=e22]: + - /placeholder: •••••••• + - link "Esqueceu a senha?" [ref=e24] [cursor=pointer]: + - /url: "#" + - button "Entrar" [ref=e25] + - paragraph [ref=e26]: + - text: Não tem conta? + - link "Criar conta" [ref=e27] [cursor=pointer]: + - /url: /register \ No newline at end of file diff --git a/.playwright-mcp/page-2026-04-02T18-39-10-569Z.png b/.playwright-mcp/page-2026-04-02T18-39-10-569Z.png new file mode 100644 index 0000000..4f0cb1c Binary files /dev/null and b/.playwright-mcp/page-2026-04-02T18-39-10-569Z.png differ diff --git a/.playwright-mcp/page-2026-04-02T18-39-22-154Z.yml b/.playwright-mcp/page-2026-04-02T18-39-22-154Z.yml new file mode 100644 index 0000000..65cfdca --- /dev/null +++ b/.playwright-mcp/page-2026-04-02T18-39-22-154Z.yml @@ -0,0 +1,29 @@ +- generic [ref=e28]: + - generic [ref=e29]: + - heading "Criar conta" [level=1] [ref=e30] + - paragraph [ref=e31]: Preencha os dados para começar a usar o Licitatche + - generic [ref=e32]: + - generic [ref=e33]: + - button "Pessoa Jurídica" [ref=e34] [cursor=pointer] + - button "Pessoa Física" [ref=e35] [cursor=pointer] + - generic [ref=e36]: + - generic [ref=e39]: Razão Social + - textbox "Razão Social" [ref=e42]: + - /placeholder: Razão Social completa + - generic [ref=e43]: + - generic [ref=e46]: CNPJ + - textbox "CNPJ" [ref=e49]: + - /placeholder: 00.000.000/0001-00 + - generic [ref=e50]: + - generic [ref=e53]: E-mail + - textbox "E-mail" [ref=e56]: + - /placeholder: voce@empresa.com.br + - generic [ref=e57]: + - generic [ref=e60]: Senha + - textbox "Senha" [ref=e63]: + - /placeholder: Mínimo 8 caracteres + - button "Criar conta" [ref=e64] + - paragraph [ref=e65]: + - text: Já tem conta? + - link "Fazer login" [ref=e66] [cursor=pointer]: + - /url: /login \ No newline at end of file diff --git a/.playwright-mcp/page-2026-04-02T18-39-51-372Z.yml b/.playwright-mcp/page-2026-04-02T18-39-51-372Z.yml new file mode 100644 index 0000000..fd1be27 --- /dev/null +++ b/.playwright-mcp/page-2026-04-02T18-39-51-372Z.yml @@ -0,0 +1,151 @@ +- generic [ref=e67]: + - complementary [ref=e68]: + - generic [ref=e72]: Licitatche + - navigation [ref=e73]: + - link "Dashboard" [ref=e74] [cursor=pointer]: + - /url: / + - generic [ref=e76]: Dashboard + - paragraph [ref=e77]: Oportunidades + - link "Todos os Editais" [ref=e78] [cursor=pointer]: + - /url: /oportunidades + - generic [ref=e80]: Todos os Editais + - link "Mapeamento" [ref=e81] [cursor=pointer]: + - /url: /oportunidades/em-analise + - generic [ref=e83]: Mapeamento + - link "Termo de Referência" [ref=e84] [cursor=pointer]: + - /url: /oportunidades/elaborando-proposta + - generic [ref=e86]: Termo de Referência + - link "Edital Publicado" [ref=e87] [cursor=pointer]: + - /url: /oportunidades/edital-publicado + - generic [ref=e89]: Edital Publicado + - link "Fase de Lances" [ref=e90] [cursor=pointer]: + - /url: /oportunidades/fase-lances + - generic [ref=e92]: Fase de Lances + - link "Recurso" [ref=e93] [cursor=pointer]: + - /url: /oportunidades/recurso + - generic [ref=e95]: Recurso + - link "Vencidas" [ref=e96] [cursor=pointer]: + - /url: /oportunidades/vencidas + - generic [ref=e98]: Vencidas + - link "Perdidas" [ref=e99] [cursor=pointer]: + - /url: /oportunidades/perdidas + - generic [ref=e101]: Perdidas + - paragraph [ref=e103]: Pipeline + - link "Kanban de Processos" [ref=e104] [cursor=pointer]: + - /url: /pipeline + - generic [ref=e106]: Kanban de Processos + - paragraph [ref=e108]: Gestão + - link "Documentos" [ref=e109] [cursor=pointer]: + - /url: /gestao/documentos + - generic [ref=e111]: Documentos + - link "Prazos" [ref=e112] [cursor=pointer]: + - /url: /gestao/prazos + - generic [ref=e114]: Prazos + - link "Órgãos Públicos" [ref=e115] [cursor=pointer]: + - /url: /gestao/orgaos + - generic [ref=e117]: Órgãos Públicos + - link "Concorrentes" [ref=e118] [cursor=pointer]: + - /url: /gestao/concorrentes + - generic [ref=e120]: Concorrentes + - link "Contratos" [ref=e121] [cursor=pointer]: + - /url: /gestao/contratos + - generic [ref=e123]: Contratos + - paragraph [ref=e125]: Inteligência + - link "Inteligência de Mercado" [ref=e126] [cursor=pointer]: + - /url: /inteligencia + - generic [ref=e128]: Inteligência de Mercado + - paragraph [ref=e130]: Sistema + - link "Usuários" [ref=e131] [cursor=pointer]: + - /url: /sistema/usuarios + - generic [ref=e133]: Usuários + - link "Configurações" [ref=e134] [cursor=pointer]: + - /url: /sistema/configuracoes + - generic [ref=e136]: Configurações + - generic [ref=e139]: + - generic [ref=e141]: a + - generic [ref=e142]: + - paragraph [ref=e143]: admin + - paragraph [ref=e144]: admin + - button [ref=e145] + - generic [ref=e148]: + - banner [ref=e149]: + - generic [ref=e150]: + - heading "Dashboard" [level=1] [ref=e151] + - paragraph [ref=e152]: Visão geral · 2 de abril de 2026 + - link "Ver Oportunidades" [ref=e154] [cursor=pointer]: + - /url: /oportunidades + - generic [ref=e155]: + - generic [ref=e156]: + - generic [ref=e157]: + - paragraph [ref=e158]: Total de Editais + - paragraph [ref=e159]: "0" + - paragraph [ref=e160]: Cadastrados + - generic [ref=e161]: + - paragraph [ref=e162]: Taxa de Vitória + - paragraph [ref=e163]: 0% + - paragraph [ref=e164]: Processos finalizados + - generic [ref=e165]: + - paragraph [ref=e166]: Contratos Ativos + - paragraph [ref=e167]: R$ 0 + - paragraph [ref=e168]: 0 contratos + - generic [ref=e169]: + - paragraph [ref=e170]: Alertas Ativos + - paragraph [ref=e171]: "0" + - paragraph [ref=e172]: Requerem atenção + - generic [ref=e173]: + - generic [ref=e174]: + - heading "Pipeline de Oportunidades" [level=3] [ref=e175] + - link "Ver kanban →" [ref=e176] [cursor=pointer]: + - /url: /pipeline + - generic [ref=e178]: + - generic [ref=e179]: + - generic [ref=e181]: "1" + - paragraph [ref=e182]: Mapeamento + - paragraph [ref=e183]: 0 editais + - generic [ref=e184]: + - generic [ref=e186]: "2" + - paragraph [ref=e187]: Termo de Referência + - paragraph [ref=e188]: 0 editais + - generic [ref=e189]: + - generic [ref=e191]: "3" + - paragraph [ref=e192]: Edital Publicado + - paragraph [ref=e193]: 0 editais + - generic [ref=e194]: + - generic [ref=e196]: "4" + - paragraph [ref=e197]: Fase de Lances + - paragraph [ref=e198]: 0 editais + - generic [ref=e199]: + - generic [ref=e201]: "5" + - paragraph [ref=e202]: Habilitação + - paragraph [ref=e203]: 0 editais + - generic [ref=e204]: + - generic [ref=e206]: "6" + - paragraph [ref=e207]: Recursos + - paragraph [ref=e208]: 0 editais + - generic [ref=e209]: + - generic [ref=e211]: "7" + - paragraph [ref=e212]: Adjudicado + - paragraph [ref=e213]: 0 editais + - generic [ref=e214]: + - generic [ref=e216]: "8" + - paragraph [ref=e217]: Contrato + - paragraph [ref=e218]: 0 editais + - generic [ref=e219]: + - generic [ref=e220]: + - generic [ref=e221]: + - heading "Alertas de Prazo" [level=3] [ref=e222] + - link "Ver todos →" [ref=e223] [cursor=pointer]: + - /url: /gestao/prazos + - paragraph [ref=e225]: Nenhum prazo urgente nos próximos 30 dias + - generic [ref=e226]: + - generic [ref=e227]: + - heading "Documentos com Alerta" [level=3] [ref=e228] + - link "Gerenciar →" [ref=e229] [cursor=pointer]: + - /url: /gestao/documentos + - paragraph [ref=e231]: Todos os documentos estão em dia + - generic [ref=e232]: + - generic [ref=e233]: + - heading "Editais Recentes" [level=3] [ref=e234] + - link "Ver todos →" [ref=e235] [cursor=pointer]: + - /url: /oportunidades + - paragraph [ref=e237]: Nenhum edital cadastrado ainda \ No newline at end of file diff --git a/.playwright-mcp/page-2026-04-02T18-39-56-786Z.png b/.playwright-mcp/page-2026-04-02T18-39-56-786Z.png new file mode 100644 index 0000000..2e03faf Binary files /dev/null and b/.playwright-mcp/page-2026-04-02T18-39-56-786Z.png differ diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..b3cec63 --- /dev/null +++ b/Makefile @@ -0,0 +1,52 @@ +.PHONY: help run stop up down db migrate api front logs + +MIGRATE=$(HOME)/go/bin/migrate + +# ─── Defaults ─────────────────────────────────────────────────────────────── +help: + @echo "" + @echo " Licitatche — comandos disponíveis" + @echo "" + @echo " make up Sobe tudo: DB + API + Front" + @echo " make down Para tudo e remove containers" + @echo " make db Sobe apenas o PostgreSQL" + @echo " make migrate Roda as migrations" + @echo " make api Sobe apenas a API Go" + @echo " make front Sobe apenas o front Nuxt" + @echo " make logs Tail dos logs da API" + @echo "" + +# ─── Infra ─────────────────────────────────────────────────────────────────── +db: + cd back-end && docker compose up -d + @echo "⏳ Aguardando PostgreSQL ficar pronto..." + @until docker exec $$(cd back-end && docker compose ps -q postgres) pg_isready -U licitatche > /dev/null 2>&1; do sleep 1; done + @echo "✅ PostgreSQL pronto" + +migrate: db + cd back-end && $(MIGRATE) -path migrations -database "postgres://licitatche:licitatche@localhost:5432/licitatche?sslmode=disable" up + +down: + cd back-end && docker compose down + +# ─── Serviços ──────────────────────────────────────────────────────────────── +api: + cd back-end && go run ./cmd/api/... + +front: + cd front-end/app && npm run dev + +# ─── Tudo junto (processos paralelos) ──────────────────────────────────────── +up: migrate + @echo "🚀 Iniciando API e Front em paralelo..." + @trap 'kill 0' SIGINT SIGTERM; \ + (cd back-end && go run ./cmd/api/... 2>&1 | sed 's/^/[api] /') & \ + (cd front-end/app && npm run dev 2>&1 | sed 's/^/[front] /') & \ + wait + +logs: + cd back-end && docker compose logs -f + +# ─── Aliases ───────────────────────────────────────────────────────────────── +run: up +stop: down diff --git a/back-end b/back-end index 6480a28..ffa085e 160000 --- a/back-end +++ b/back-end @@ -1 +1 @@ -Subproject commit 6480a285f5a0ad3f0c96500f4bb6fe2403ffb28a +Subproject commit ffa085e6ade3e1fd96cb882fff5e1b71c4f683cc diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml new file mode 100644 index 0000000..c16540c --- /dev/null +++ b/docker-compose.prod.yml @@ -0,0 +1,100 @@ +services: + postgres: + image: postgres:16-alpine + restart: unless-stopped + environment: + POSTGRES_USER: ${DB_USER:-licitatche} + POSTGRES_PASSWORD: ${DB_PASSWORD:-licitatche} + POSTGRES_DB: ${DB_NAME:-licitatche} + volumes: + - postgres_data:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${DB_USER:-licitatche}"] + interval: 5s + timeout: 5s + retries: 5 + networks: + - internal + + migrate: + image: migrate/migrate:v4.17.0 + depends_on: + postgres: + condition: service_healthy + volumes: + - ./back-end/migrations:/migrations + command: [ + "-path", "/migrations", + "-database", "postgres://${DB_USER:-licitatche}:${DB_PASSWORD:-licitatche}@postgres:5432/${DB_NAME:-licitatche}?sslmode=disable", + "up" + ] + networks: + - internal + + api: + build: + context: ./back-end + restart: unless-stopped + depends_on: + postgres: + condition: service_healthy + migrate: + condition: service_completed_successfully + environment: + APP_PORT: "8080" + ALLOWED_ORIGINS: ${ALLOWED_ORIGINS:-http://localhost:3000} + DB_HOST: postgres + DB_PORT: "5432" + DB_USER: ${DB_USER:-licitatche} + DB_PASSWORD: ${DB_PASSWORD:-licitatche} + DB_NAME: ${DB_NAME:-licitatche} + JWT_SECRET: ${JWT_SECRET} + JWT_REFRESH_SECRET: ${JWT_REFRESH_SECRET} + OPENAI_API_KEY: ${OPENAI_API_KEY:-} + FILES_DIR: /app/data/files + volumes: + - api_files:/app/data/files + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:8080/api/v1/editais", "-o", "/dev/null", "-s", "-w", "%{http_code}", "||", "true"] + interval: 10s + timeout: 5s + retries: 3 + start_period: 10s + networks: + - internal + + front: + build: + context: ./front-end + args: + NUXT_PUBLIC_API_BASE: ${PUBLIC_API_BASE:-http://163.176.236.167:8080/api/v1} + restart: unless-stopped + depends_on: + - api + environment: + NUXT_PUBLIC_API_BASE: ${PUBLIC_API_BASE:-http://163.176.236.167:8080/api/v1} + ports: + - "3000:3000" + networks: + - internal + + nginx: + image: nginx:alpine + restart: unless-stopped + depends_on: + - api + - front + ports: + - "80:80" + volumes: + - ./nginx.conf:/etc/nginx/conf.d/default.conf:ro + networks: + - internal + +volumes: + postgres_data: + api_files: + +networks: + internal: + driver: bridge diff --git a/edital-real.pdf b/edital-real.pdf new file mode 100644 index 0000000..12c4f40 Binary files /dev/null and b/edital-real.pdf differ diff --git a/edital-teste.pdf b/edital-teste.pdf new file mode 100644 index 0000000..359059c --- /dev/null +++ b/edital-teste.pdf @@ -0,0 +1,74 @@ +%PDF-1.3 +% ReportLab Generated PDF document (opensource) +1 0 obj +<< +/F1 2 0 R /F2 3 0 R +>> +endobj +2 0 obj +<< +/BaseFont /Helvetica /Encoding /WinAnsiEncoding /Name /F1 /Subtype /Type1 /Type /Font +>> +endobj +3 0 obj +<< +/BaseFont /Helvetica-Bold /Encoding /WinAnsiEncoding /Name /F2 /Subtype /Type1 /Type /Font +>> +endobj +4 0 obj +<< +/Contents 8 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 7 0 R /Resources << +/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] +>> /Rotate 0 /Trans << + +>> + /Type /Page +>> +endobj +5 0 obj +<< +/PageMode /UseNone /Pages 7 0 R /Type /Catalog +>> +endobj +6 0 obj +<< +/Author (anonymous) /CreationDate (D:20260323201142-03'00') /Creator (anonymous) /Keywords () /ModDate (D:20260323201142-03'00') /Producer (ReportLab PDF Library - \(opensource\)) + /Subject (unspecified) /Title (untitled) /Trapped /False +>> +endobj +7 0 obj +<< +/Count 1 /Kids [ 4 0 R ] /Type /Pages +>> +endobj +8 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 564 +>> +stream +GasJO:N)aW&B4,6'^sC9>HM"s/DHED-Da_=,cnGMpAm@<;o$m@rVR]8U^9pkKS7:ZqosBB6UL8ga*Z>:QNag)-"Mto*c`(a:g>4qcGk/=\KC.0U\MAPB7ps#T+Z^L_>E7Aqf^&C?"_,*^\rVHj#2MA-/2P?q[Y(r;1/:sID+G<)c,,a`7,J[VDUPNB.ObU#PC\BR1$-aZo;#_D%nFiiop":=_+oMcF0UUs-N1-k18Xh%Q;;bGAk-n=KOCmrh-Pkf*>4)C!S[:/(b=P3C;V`mJI<=5J]u2OFnO%3"9t^@$Lm;d[=KuPI=F.gJT&oi-#e%-e6cko>*7G=,PHg`"C<6#pD2nX8qBGJ"]-F0ZUo/Gq_F!$CP_Lq()FroO=daPhLiJ;2Y/.D07V;qH1*%hR\Ga=1hg/5'jG_U=-/WkMYIcfE^4:hOu]UC0k>$Nn7]0"$rTH%VgIVW*t6_nL6JjkU\FT3*BVhK'DWcjVrYIe\,\M[>$KVVJWhejB#ZXE9P&LG:b&FK;bN-_f.Vp3t-rLY_!.%M^Ug1Gf"206;]N^Wh$4)#~>endstream +endobj +xref +0 9 +0000000000 65535 f +0000000061 00000 n +0000000102 00000 n +0000000209 00000 n +0000000321 00000 n +0000000524 00000 n +0000000592 00000 n +0000000853 00000 n +0000000912 00000 n +trailer +<< +/ID +[] +% ReportLab generated PDF document -- digest (opensource) + +/Info 6 0 R +/Root 5 0 R +/Size 9 +>> +startxref +1566 +%%EOF diff --git a/front-end/.dockerignore b/front-end/.dockerignore new file mode 100644 index 0000000..6ac1524 --- /dev/null +++ b/front-end/.dockerignore @@ -0,0 +1,5 @@ +node_modules +.nuxt +.data +app +public diff --git a/front-end/Dockerfile b/front-end/Dockerfile new file mode 100644 index 0000000..fb21a1d --- /dev/null +++ b/front-end/Dockerfile @@ -0,0 +1,13 @@ +FROM node:22-alpine + +RUN apk add --no-cache curl +ENV NODE_ENV=production +ENV TZ=America/Sao_Paulo + +WORKDIR /app +COPY .output ./.output + +RUN cd .output/server && npm install --omit=dev + +EXPOSE 3000 +CMD ["node", ".output/server/index.mjs"] diff --git a/front-end/app/components/AppSidebar.vue b/front-end/app/components/AppSidebar.vue index f042232..d749830 100644 --- a/front-end/app/components/AppSidebar.vue +++ b/front-end/app/components/AppSidebar.vue @@ -1,21 +1,33 @@ + + diff --git a/front-end/app/composables/useApi.ts b/front-end/app/composables/useApi.ts index 55dba40..ea0402c 100644 --- a/front-end/app/composables/useApi.ts +++ b/front-end/app/composables/useApi.ts @@ -1,16 +1,45 @@ // Wrapper sobre $fetch que injeta o Authorization header automaticamente. +// Em caso de 401, tenta renovar o token via refresh e repete a requisição. export function useApi() { const { public: { apiBase } } = useRuntimeConfig() const token = useCookie('auth_token') + const refreshToken = useCookie('refresh_token') - function apiFetch(path: string, options: Parameters[1] = {}): Promise { - return $fetch(`${apiBase}${path}`, { + async function tryRefresh(): Promise { + if (!refreshToken.value) return false + try { + const res = await $fetch<{ access_token: string; refresh_token: string }>( + `${apiBase}/auth/refresh`, + { method: 'POST', body: { refresh_token: refreshToken.value } } + ) + token.value = res.access_token + refreshToken.value = res.refresh_token + return true + } catch { + token.value = null + refreshToken.value = null + navigateTo('/login') + return false + } + } + + async function apiFetch(path: string, options: Parameters[1] = {}): Promise { + const doFetch = () => $fetch(`${apiBase}${path}`, { ...options, headers: { ...(options.headers as Record || {}), ...(token.value ? { Authorization: `Bearer ${token.value}` } : {}), }, }) + + try { + return await doFetch() + } catch (err: any) { + if (err?.status === 401 && await tryRefresh()) { + return await doFetch() + } + throw err + } } return { apiFetch } diff --git a/front-end/app/composables/useAuth.ts b/front-end/app/composables/useAuth.ts index 87eacfc..1bff5e0 100644 --- a/front-end/app/composables/useAuth.ts +++ b/front-end/app/composables/useAuth.ts @@ -4,6 +4,17 @@ interface AuthUser { papel: string } +export interface TenantOption { + id: string + name: string + slug: string +} + +export type LoginResult = + | { success: true } + | { success: false; error: string } + | { needsTenantSelect: true; tenants: TenantOption[]; email: string; password: string } + export function useAuth() { const { public: { apiBase } } = useRuntimeConfig() @@ -23,23 +34,48 @@ export function useAuth() { } } - async function login(email: string, password: string, slug: string): Promise<{ success: boolean; error?: string }> { + function _applyTokens(access: string, refresh: string) { + token.value = access + refreshToken.value = refresh + const payload = JSON.parse(atob(access.split('.')[1])) + user.value = { nome: payload.email.split('@')[0], email: payload.email, papel: payload.role } + } + + async function login(email: string, password: string): Promise { try { - const res = await $fetch<{ access_token: string; refresh_token: string }>(`${apiBase}/auth/login`, { + const res = await $fetch<{ + tokens?: { access_token: string; refresh_token: string } + tenants?: TenantOption[] + }>(`${apiBase}/auth/login`, { method: 'POST', - body: { email, password, slug }, + body: { email, password }, }) - token.value = res.access_token - refreshToken.value = res.refresh_token + if (res.tokens) { + _applyTokens(res.tokens.access_token, res.tokens.refresh_token) + return { success: true } + } - const payload = JSON.parse(atob(res.access_token.split('.')[1])) - user.value = { nome: payload.email.split('@')[0], email: payload.email, papel: payload.role } + if (res.tenants && res.tenants.length > 0) { + return { needsTenantSelect: true, tenants: res.tenants, email, password } + } + return { success: false, error: 'Resposta inesperada do servidor.' } + } catch (err: any) { + return { success: false, error: err?.data?.error || 'E-mail ou senha incorretos.' } + } + } + + async function selectTenant(email: string, password: string, tenantId: string): Promise<{ success: boolean; error?: string }> { + try { + const res = await $fetch<{ access_token: string; refresh_token: string }>(`${apiBase}/auth/login/select-tenant`, { + method: 'POST', + body: { email, password, tenant_id: tenantId }, + }) + _applyTokens(res.access_token, res.refresh_token) return { success: true } } catch (err: any) { - const msg = err?.data?.error || 'E-mail, senha ou organização incorretos.' - return { success: false, error: msg } + return { success: false, error: err?.data?.error || 'Erro ao selecionar empresa.' } } } @@ -57,5 +93,5 @@ export function useAuth() { navigateTo('/login') } - return { user, isAuthenticated, login, logout } + return { user, isAuthenticated, login, selectTenant, logout } } diff --git a/front-end/app/pages/gestao/documentos.vue b/front-end/app/pages/gestao/documentos.vue index 43118f4..480265b 100644 --- a/front-end/app/pages/gestao/documentos.vue +++ b/front-end/app/pages/gestao/documentos.vue @@ -1,6 +1,8 @@ diff --git a/front-end/app/pages/index.vue b/front-end/app/pages/index.vue index 55365fe..a551bcf 100644 --- a/front-end/app/pages/index.vue +++ b/front-end/app/pages/index.vue @@ -1,65 +1,232 @@ diff --git a/front-end/app/pages/login.vue b/front-end/app/pages/login.vue index 09d0c06..e088f40 100644 --- a/front-end/app/pages/login.vue +++ b/front-end/app/pages/login.vue @@ -1,23 +1,50 @@ @@ -52,17 +79,6 @@ async function handleSubmit() { /> - - - - @@ -84,6 +100,29 @@ async function handleSubmit() {

+ + + diff --git a/front-end/app/pages/oportunidades/[id].vue b/front-end/app/pages/oportunidades/[id].vue new file mode 100644 index 0000000..9176522 --- /dev/null +++ b/front-end/app/pages/oportunidades/[id].vue @@ -0,0 +1,1097 @@ + + + +