Compare commits

...

102 Commits

Author SHA1 Message Date
89065c9db6 fix: combo de idioma com icones
All checks were successful
Deploy QR Rapido / test (push) Successful in 1m3s
Deploy QR Rapido / build-and-push (push) Successful in 9m20s
Deploy QR Rapido / deploy-staging (push) Has been skipped
Deploy QR Rapido / deploy-production (push) Successful in 2m15s
2026-03-09 12:01:15 -03:00
9c393f01e5 fix: trocar es-PY por es 2026-03-09 11:45:55 -03:00
6301e33686 feat: adicionei inglês. 2026-03-08 19:39:33 -03:00
311efcae62 fix: deplos testes
All checks were successful
Deploy QR Rapido / test (push) Successful in 3m50s
Deploy QR Rapido / build-and-push (push) Successful in 10m40s
Deploy QR Rapido / deploy-staging (push) Has been skipped
Deploy QR Rapido / deploy-production (push) Successful in 3m6s
2026-03-08 12:51:09 -03:00
7a0c12f8d2 feat: api separada do front-end e area do desenvolvedor.
Some checks failed
Deploy QR Rapido / test (push) Failing after 17s
Deploy QR Rapido / build-and-push (push) Has been skipped
Deploy QR Rapido / deploy-staging (push) Has been skipped
Deploy QR Rapido / deploy-production (push) Has been skipped
2026-03-08 12:40:51 -03:00
e523ade864 fix: build
All checks were successful
Deploy QR Rapido / test (push) Successful in 58s
Deploy QR Rapido / build-and-push (push) Successful in 14m17s
Deploy QR Rapido / deploy-staging (push) Has been skipped
Deploy QR Rapido / deploy-production (push) Successful in 2m55s
2026-02-15 21:11:48 -03:00
7dccfc10f0 fix: ajustes de rota
Some checks failed
Deploy QR Rapido / test (push) Failing after 1m18s
Deploy QR Rapido / build-and-push (push) Has been skipped
Deploy QR Rapido / deploy-staging (push) Has been skipped
Deploy QR Rapido / deploy-production (push) Has been skipped
2026-02-15 21:05:57 -03:00
72bbbeea4a fix: validações de tipo, receber na conta da empresa, og image e idioma.
All checks were successful
Deploy QR Rapido / test (push) Successful in 1m1s
Deploy QR Rapido / build-and-push (push) Successful in 16m7s
Deploy QR Rapido / deploy-staging (push) Has been skipped
Deploy QR Rapido / deploy-production (push) Successful in 2m12s
2026-01-28 10:54:15 -03:00
16a9720a12 feat: qrcode por creditos.
All checks were successful
Deploy QR Rapido / test (push) Successful in 59s
Deploy QR Rapido / build-and-push (push) Successful in 9m57s
Deploy QR Rapido / deploy-staging (push) Has been skipped
Deploy QR Rapido / deploy-production (push) Successful in 2m11s
2026-01-26 20:13:45 -03:00
162e28ae5a fix: PIX + idioma espanhol e SEO
All checks were successful
Deploy QR Rapido / test (push) Successful in 43s
Deploy QR Rapido / build-and-push (push) Successful in 16m43s
Deploy QR Rapido / deploy-staging (push) Has been skipped
Deploy QR Rapido / deploy-production (push) Successful in 1m55s
2026-01-25 12:04:24 -03:00
bdf78ed418 fix: restaurar carregamento de Docker Secrets no Program.cs
All checks were successful
Deploy QR Rapido / test (push) Successful in 45s
Deploy QR Rapido / build-and-push (push) Successful in 16m19s
Deploy QR Rapido / deploy-staging (push) Has been skipped
Deploy QR Rapido / deploy-production (push) Successful in 2m12s
A linha builder.Configuration.AddDockerSecrets() foi perdida durante
o merge, causando HTTP 500 em produção porque os secrets não são
carregados e valores como "LOADED_FROM_DOCKER_SECRET" são usados
diretamente na conexão MongoDB/Stripe/OAuth.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-25 10:26:57 -03:00
dbd3c8851b fix: ajuste de warnings e combos
All checks were successful
Deploy QR Rapido / test (push) Successful in 3m50s
Deploy QR Rapido / build-and-push (push) Successful in 16m49s
Deploy QR Rapido / deploy-staging (push) Has been skipped
Deploy QR Rapido / deploy-production (push) Successful in 2m12s
2026-01-25 00:44:34 -03:00
4a7cdbf26d Merge branch 'main' of http://git.carneiro.ddnsfree.com/ricardo/QrRapido
All checks were successful
Deploy QR Rapido / test (push) Successful in 1m4s
Deploy QR Rapido / build-and-push (push) Successful in 18m20s
Deploy QR Rapido / deploy-staging (push) Has been skipped
Deploy QR Rapido / deploy-production (push) Successful in 1m55s
2026-01-25 00:10:52 -03:00
55d18adc74 feat: pix!!! 2026-01-25 00:05:26 -03:00
33c930bf94 fix: ajustes 2026-01-24 21:52:05 -03:00
262a64db16 fix: ajustes
All checks were successful
Deploy QR Rapido / test (push) Successful in 46s
Deploy QR Rapido / build-and-push (push) Successful in 15m10s
Deploy QR Rapido / deploy-staging (push) Has been skipped
Deploy QR Rapido / deploy-production (push) Successful in 2m12s
2026-01-24 21:50:33 -03:00
16e81cd941 chore: backup dos secrets de produção (hardcoded)
- create-secrets-prod.sh: Script pronto para criar os secrets no Swarm
- secrets-prod.env: Backup dos valores em formato .env

ATENÇÃO: Valores reais - repositório privado apenas!

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-24 21:43:34 -03:00
6d4a8904f2 feat: Docker Secrets para credenciais sensíveis
- Criado DockerSecretsConfigurationProvider para ler secrets de /run/secrets/
- Removidas credenciais sensíveis do appsettings.Production.json
- Adicionado indicador visual no rodapé (✓/✗) para verificar se secrets foram carregados
- Atualizado deploy.yml para usar Docker Secrets no Swarm
- Criado script create-docker-secrets.sh para gerenciar secrets
- Criado template secrets.env.template para facilitar configuração
- Documentação completa em DOCKER_SECRETS_SETUP.md

Secrets gerenciados:
- stripe_secret_key
- stripe_webhook_secret
- mongodb_connection_string
- google_client_id / google_client_secret
- microsoft_client_id / microsoft_client_secret

IMPORTANTE: Após este deploy, é necessário criar os secrets no Swarm
e recriar o service. Consulte DOCKER_SECRETS_SETUP.md.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-24 21:39:20 -03:00
00c5a132ab fix: redirect loop infinito em URLs com cultura
All checks were successful
Deploy QR Rapido / test (push) Successful in 4m46s
Deploy QR Rapido / build-and-push (push) Successful in 12m20s
Deploy QR Rapido / deploy-staging (push) Has been skipped
Deploy QR Rapido / deploy-production (push) Successful in 3m22s
O middleware redirecionava /pt-BR/* para /pt-BR/* causando
ERR_TOO_MANY_REDIRECTS. Adicionada verificação case-sensitive
para evitar redirect quando URL já está na forma canônica.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-24 21:05:50 -03:00
ea6eacc6c6 fix: google search
All checks were successful
Deploy QR Rapido / test (push) Successful in 4m22s
Deploy QR Rapido / build-and-push (push) Successful in 9m2s
Deploy QR Rapido / deploy-staging (push) Has been skipped
Deploy QR Rapido / deploy-production (push) Successful in 2m52s
2025-11-14 21:46:21 -03:00
707dab8075 fix: robots.txt
All checks were successful
Deploy QR Rapido / test (push) Successful in 3m53s
Deploy QR Rapido / build-and-push (push) Successful in 14m49s
Deploy QR Rapido / deploy-staging (push) Has been skipped
Deploy QR Rapido / deploy-production (push) Successful in 2m16s
2025-11-02 13:47:26 -03:00
05d4a83b7d fix: gtag com scritp do google
All checks were successful
Deploy QR Rapido / test (push) Successful in 34s
Deploy QR Rapido / build-and-push (push) Successful in 15m0s
Deploy QR Rapido / deploy-staging (push) Has been skipped
Deploy QR Rapido / deploy-production (push) Successful in 2m11s
2025-10-23 22:53:09 -03:00
262870548a fix: gtag
All checks were successful
Deploy QR Rapido / test (push) Successful in 33s
Deploy QR Rapido / build-and-push (push) Successful in 15m10s
Deploy QR Rapido / deploy-staging (push) Has been skipped
Deploy QR Rapido / deploy-production (push) Successful in 2m11s
2025-10-23 19:55:24 -03:00
174287f5bf fix: google tag manager
All checks were successful
Deploy QR Rapido / test (push) Successful in 39s
Deploy QR Rapido / build-and-push (push) Successful in 15m7s
Deploy QR Rapido / deploy-staging (push) Has been skipped
Deploy QR Rapido / deploy-production (push) Successful in 2m14s
2025-10-23 13:23:37 -03:00
7acd78e2c3 fix: ajuste google analytics 2025-10-23 01:43:36 -03:00
6d5e8dadea feat: clarity e form proprio de avaliação 2025-10-23 01:34:07 -03:00
cd9380bdc2 fix: Tentar ms clarity
All checks were successful
Deploy QR Rapido / test (push) Successful in 1m43s
Deploy QR Rapido / build-and-push (push) Successful in 14m22s
Deploy QR Rapido / deploy-staging (push) Has been skipped
Deploy QR Rapido / deploy-production (push) Successful in 1m59s
2025-10-22 22:00:12 -03:00
8e39544463 fix: hotjar no vivaldi 2025-10-22 21:02:21 -03:00
251cbe56a4 feat: fale conosco
All checks were successful
Deploy QR Rapido / test (push) Successful in 45s
Deploy QR Rapido / build-and-push (push) Successful in 14m58s
Deploy QR Rapido / deploy-staging (push) Has been skipped
Deploy QR Rapido / deploy-production (push) Successful in 2m12s
2025-10-21 22:45:52 -03:00
916838820a feat: publicar/remover tawk.to
All checks were successful
Deploy QR Rapido / test (push) Successful in 51s
Deploy QR Rapido / build-and-push (push) Successful in 14m8s
Deploy QR Rapido / deploy-staging (push) Has been skipped
Deploy QR Rapido / deploy-production (push) Successful in 1m55s
2025-10-21 22:05:31 -03:00
5a90dc1570 feat: tawk.to
All checks were successful
Deploy QR Rapido / test (push) Successful in 39s
Deploy QR Rapido / build-and-push (push) Successful in 13m22s
Deploy QR Rapido / deploy-staging (push) Has been skipped
Deploy QR Rapido / deploy-production (push) Successful in 2m10s
2025-10-21 21:35:59 -03:00
65c2e04589 fix:ids prod
All checks were successful
Deploy QR Rapido / test (push) Successful in 41s
Deploy QR Rapido / build-and-push (push) Successful in 10m38s
Deploy QR Rapido / deploy-staging (push) Has been skipped
Deploy QR Rapido / deploy-production (push) Successful in 1m56s
2025-10-21 20:00:33 -03:00
a006975ef5 feat: rodape e planos
All checks were successful
Deploy QR Rapido / test (push) Successful in 41s
Deploy QR Rapido / build-and-push (push) Successful in 8m59s
Deploy QR Rapido / deploy-staging (push) Has been skipped
Deploy QR Rapido / deploy-production (push) Successful in 2m8s
2025-10-21 17:46:01 -03:00
eb0751cb16 feat: Tipos de qrcodes (bolinhas, arredondado ou quadrado)
All checks were successful
Deploy QR Rapido / test (push) Successful in 1m3s
Deploy QR Rapido / build-and-push (push) Successful in 8m23s
Deploy QR Rapido / deploy-staging (push) Has been skipped
Deploy QR Rapido / deploy-production (push) Successful in 3m34s
2025-10-21 17:02:26 -03:00
8541e68711 feat: qrcode com contador de leituras 2025-10-21 16:31:54 -03:00
2edb4e1196 fix: ajustar para configuração de plano ficam só no mongondb.
All checks were successful
Deploy QR Rapido / test (push) Successful in 41s
Deploy QR Rapido / build-and-push (push) Successful in 14m6s
Deploy QR Rapido / deploy-staging (push) Has been skipped
Deploy QR Rapido / deploy-production (push) Successful in 1m55s
2025-10-19 21:48:45 -03:00
59e04fedc7 feat: hotjar
All checks were successful
Deploy QR Rapido / test (push) Successful in 44s
Deploy QR Rapido / build-and-push (push) Successful in 13m49s
Deploy QR Rapido / deploy-staging (push) Has been skipped
Deploy QR Rapido / deploy-production (push) Successful in 2m10s
2025-10-19 10:21:30 -03:00
e95a435108 feat: hotjar 2025-10-19 10:10:12 -03:00
aa7a80ff61 fix: produtos stripe
All checks were successful
Deploy QR Rapido / test (push) Successful in 42s
Deploy QR Rapido / build-and-push (push) Successful in 14m38s
Deploy QR Rapido / deploy-staging (push) Has been skipped
Deploy QR Rapido / deploy-production (push) Successful in 1m55s
2025-10-19 00:43:26 -03:00
7a552315c2 fix: recursos reais
All checks were successful
Deploy QR Rapido / test (push) Successful in 1m0s
Deploy QR Rapido / build-and-push (push) Successful in 13m14s
Deploy QR Rapido / deploy-staging (push) Has been skipped
Deploy QR Rapido / deploy-production (push) Successful in 2m9s
2025-10-19 00:12:05 -03:00
2e95b2a488 fix: arquivos .md
All checks were successful
Deploy QR Rapido / test (push) Successful in 3m55s
Deploy QR Rapido / build-and-push (push) Successful in 13m19s
Deploy QR Rapido / deploy-staging (push) Has been skipped
Deploy QR Rapido / deploy-production (push) Successful in 2m41s
2025-10-18 23:48:13 -03:00
8b3da7cb0a feat: Criação de tutoriais e remoçaõ de anuncios.
All checks were successful
Deploy QR Rapido / test (push) Successful in 3m59s
Deploy QR Rapido / build-and-push (push) Successful in 12m31s
Deploy QR Rapido / deploy-staging (push) Has been skipped
Deploy QR Rapido / deploy-production (push) Successful in 2m17s
2025-10-18 23:18:12 -03:00
232d4d6c54 feat: ajustes e artigos 2025-10-13 19:50:31 -03:00
Ricardo Carneiro
2ba9a675b5 fix: logs 2025-09-22 23:50:43 -03:00
Ricardo Carneiro
000ac967e1 fix: erro de userstats
All checks were successful
Deploy QR Rapido / test (push) Successful in 59s
Deploy QR Rapido / build-and-push (push) Successful in 9m51s
Deploy QR Rapido / deploy-staging (push) Has been skipped
Deploy QR Rapido / deploy-production (push) Successful in 1m20s
2025-09-22 22:15:26 -03:00
Ricardo Carneiro
822dd424c7 fix: images
All checks were successful
Deploy QR Rapido / test (push) Successful in 39s
Deploy QR Rapido / build-and-push (push) Successful in 13m12s
Deploy QR Rapido / deploy-staging (push) Has been skipped
Deploy QR Rapido / deploy-production (push) Successful in 1m51s
2025-09-22 21:39:53 -03:00
Ricardo Carneiro
233e7ec40f fix: images
All checks were successful
Deploy QR Rapido / test (push) Successful in 42s
Deploy QR Rapido / build-and-push (push) Successful in 8m27s
Deploy QR Rapido / deploy-staging (push) Has been skipped
Deploy QR Rapido / deploy-production (push) Successful in 1m23s
2025-09-22 21:18:48 -03:00
b54aa295ac fix: add Node.js to Docker build stage for frontend compilation
All checks were successful
Deploy QR Rapido / test (push) Successful in 44s
Deploy QR Rapido / build-and-push (push) Successful in 12m59s
Deploy QR Rapido / deploy-staging (push) Has been skipped
Deploy QR Rapido / deploy-production (push) Successful in 1m24s
- Install Node.js 18.x in Docker build stage
- Add MongoDB DataProtection for Swarm compatibility
- Enables shared authentication keys across multiple replicas

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-22 16:25:54 -03:00
Ricardo Carneiro
870436c1ab fix: swarm e ajustes de login
Some checks failed
Deploy QR Rapido / test (push) Successful in 3m57s
Deploy QR Rapido / build-and-push (push) Failing after 7m53s
Deploy QR Rapido / deploy-staging (push) Has been skipped
Deploy QR Rapido / deploy-production (push) Has been skipped
2025-09-22 16:12:10 -03:00
Ricardo Carneiro
1b74de34e6 fix: ajustes de javascript e funcionamento
Some checks failed
Deploy QR Rapido / test (push) Successful in 4m11s
Deploy QR Rapido / build-and-push (push) Failing after 5m52s
Deploy QR Rapido / deploy-staging (push) Has been skipped
Deploy QR Rapido / deploy-production (push) Has been skipped
2025-09-22 14:54:43 -03:00
Ricardo Carneiro
b4b87f42c1 fix: teste de deploy
All checks were successful
Deploy QR Rapido / test (push) Successful in 55s
Deploy QR Rapido / build-and-push (push) Successful in 6m35s
Deploy QR Rapido / deploy-staging (push) Has been skipped
Deploy QR Rapido / deploy-production (push) Successful in 1m19s
2025-09-21 22:02:30 -03:00
Ricardo Carneiro
9ba6da6270 fix: ajustes performance
All checks were successful
Deploy QR Rapido / test (push) Successful in 45s
Deploy QR Rapido / build-and-push (push) Successful in 13m11s
Deploy QR Rapido / deploy-staging (push) Has been skipped
Deploy QR Rapido / deploy-production (push) Successful in 1m20s
2025-09-21 01:48:26 -03:00
Ricardo Carneiro
552ae6fd10 fix: performance e otimizações
All checks were successful
Deploy QR Rapido / test (push) Successful in 4m9s
Deploy QR Rapido / build-and-push (push) Successful in 12m51s
Deploy QR Rapido / deploy-staging (push) Has been skipped
Deploy QR Rapido / deploy-production (push) Successful in 1m21s
2025-09-21 00:31:52 -03:00
Ricardo Carneiro
5ba0d62595 fix: ajustes diversos
All checks were successful
Deploy QR Rapido / test (push) Successful in 3m51s
Deploy QR Rapido / build-and-push (push) Successful in 14m7s
Deploy QR Rapido / deploy-staging (push) Has been skipped
Deploy QR Rapido / deploy-production (push) Successful in 1m41s
2025-09-20 22:46:08 -03:00
Ricardo Carneiro
49da9f874a fix: ajustes de navegação
All checks were successful
Deploy QR Rapido / test (push) Successful in 52s
Deploy QR Rapido / build-and-push (push) Successful in 12m53s
Deploy QR Rapido / deploy-staging (push) Has been skipped
Deploy QR Rapido / deploy-production (push) Successful in 1m40s
2025-09-04 12:31:56 -03:00
Ricardo Carneiro
71c575f879 fix; ads.txt
All checks were successful
Deploy QR Rapido / test (push) Successful in 1m9s
Deploy QR Rapido / build-and-push (push) Successful in 13m8s
Deploy QR Rapido / deploy-staging (push) Has been skipped
Deploy QR Rapido / deploy-production (push) Successful in 1m35s
2025-08-28 20:34:26 -03:00
Ricardo Carneiro
81ff8d2ae3 fix: mogo pool e limits
All checks were successful
Deploy QR Rapido / test (push) Successful in 32s
Deploy QR Rapido / build-and-push (push) Successful in 7m7s
Deploy QR Rapido / deploy-staging (push) Has been skipped
Deploy QR Rapido / deploy-production (push) Successful in 1m38s
2025-08-25 21:32:28 -03:00
Ricardo Carneiro
e960e4af03 fix: health mapeado errado.
All checks were successful
Deploy QR Rapido / test (push) Successful in 45s
Deploy QR Rapido / build-and-push (push) Successful in 7m10s
Deploy QR Rapido / deploy-staging (push) Has been skipped
Deploy QR Rapido / deploy-production (push) Successful in 1m35s
2025-08-25 20:20:12 -03:00
Ricardo Carneiro
51f0820668 fix: ratelimit e erros de mongodb
All checks were successful
Deploy QR Rapido / test (push) Successful in 35s
Deploy QR Rapido / build-and-push (push) Successful in 6m58s
Deploy QR Rapido / deploy-staging (push) Has been skipped
Deploy QR Rapido / deploy-production (push) Successful in 1m35s
2025-08-25 20:00:50 -03:00
Ricardo Carneiro
d8f3b97c32 fix: keys no servidor 2 tb
All checks were successful
Deploy QR Rapido / test (push) Successful in 42s
Deploy QR Rapido / build-and-push (push) Successful in 6m56s
Deploy QR Rapido / deploy-staging (push) Has been skipped
Deploy QR Rapido / deploy-production (push) Successful in 1m36s
2025-08-25 18:32:17 -03:00
Ricardo Carneiro
0868245893 fix: volume de chaves
All checks were successful
Deploy QR Rapido / test (push) Successful in 37s
Deploy QR Rapido / build-and-push (push) Successful in 6m59s
Deploy QR Rapido / deploy-staging (push) Has been skipped
Deploy QR Rapido / deploy-production (push) Successful in 1m38s
2025-08-25 16:29:06 -03:00
Ricardo Carneiro
c59e6711c0 fix: authcontroller
All checks were successful
Deploy QR Rapido / test (push) Successful in 54s
Deploy QR Rapido / build-and-push (push) Successful in 7m1s
Deploy QR Rapido / deploy-staging (push) Has been skipped
Deploy QR Rapido / deploy-production (push) Successful in 1m36s
2025-08-25 00:02:40 -03:00
Ricardo Carneiro
b8ab7948a9 fix: cache
All checks were successful
Deploy QR Rapido / test (push) Successful in 40s
Deploy QR Rapido / build-and-push (push) Successful in 13m14s
Deploy QR Rapido / deploy-staging (push) Has been skipped
Deploy QR Rapido / deploy-production (push) Successful in 1m38s
2025-08-24 23:30:45 -03:00
Ricardo Carneiro
0ca91fb6df feat: stateless login ms/google
All checks were successful
Deploy QR Rapido / test (push) Successful in 42s
Deploy QR Rapido / build-and-push (push) Successful in 13m32s
Deploy QR Rapido / deploy-staging (push) Has been skipped
Deploy QR Rapido / deploy-production (push) Successful in 1m38s
2025-08-24 23:08:34 -03:00
Ricardo Carneiro
3dede8b4d6 fix: ip do seq
All checks were successful
Deploy QR Rapido / test (push) Successful in 50s
Deploy QR Rapido / build-and-push (push) Successful in 13m43s
Deploy QR Rapido / deploy-staging (push) Has been skipped
Deploy QR Rapido / deploy-production (push) Successful in 1m18s
2025-08-24 21:32:45 -03:00
Ricardo Carneiro
74d9801713 fix: conectar ao seq do host do docker.
All checks were successful
Deploy QR Rapido / test (push) Successful in 39s
Deploy QR Rapido / build-and-push (push) Successful in 13m35s
Deploy QR Rapido / deploy-staging (push) Has been skipped
Deploy QR Rapido / deploy-production (push) Successful in 1m17s
2025-08-24 20:04:16 -03:00
Ricardo Carneiro
1dc1a5b897 fix: remover seq do healthcheck
Some checks failed
Deploy QR Rapido / deploy-staging (push) Blocked by required conditions
Deploy QR Rapido / deploy-production (push) Blocked by required conditions
Deploy QR Rapido / test (push) Successful in 48s
Deploy QR Rapido / build-and-push (push) Has been cancelled
2025-08-24 19:54:08 -03:00
Ricardo Carneiro
346aa86216 fix: program.cs
All checks were successful
Deploy QR Rapido / test (push) Successful in 27s
Deploy QR Rapido / build-and-push (push) Successful in 6m52s
Deploy QR Rapido / deploy-staging (push) Has been skipped
Deploy QR Rapido / deploy-production (push) Successful in 1m18s
2025-08-21 22:25:32 -03:00
Ricardo Carneiro
447d2863f4 fix: program.cs para login ms
Some checks failed
Deploy QR Rapido / test (push) Failing after 21s
Deploy QR Rapido / build-and-push (push) Has been skipped
Deploy QR Rapido / deploy-staging (push) Has been skipped
Deploy QR Rapido / deploy-production (push) Has been skipped
2025-08-21 22:16:00 -03:00
Ricardo Carneiro
07c36eb8cd fix: cores dos botões
Some checks failed
Deploy QR Rapido / build-and-push (push) Blocked by required conditions
Deploy QR Rapido / deploy-staging (push) Blocked by required conditions
Deploy QR Rapido / deploy-production (push) Blocked by required conditions
Deploy QR Rapido / test (push) Has been cancelled
2025-08-21 22:14:39 -03:00
Ricardo Carneiro
6aafb1d067 feat: ajustes de pagamento e account/logoff
All checks were successful
Deploy QR Rapido / test (push) Successful in 3m34s
Deploy QR Rapido / build-and-push (push) Successful in 7m33s
Deploy QR Rapido / deploy-staging (push) Has been skipped
Deploy QR Rapido / deploy-production (push) Successful in 1m20s
2025-08-21 22:00:56 -03:00
Ricardo Carneiro
52849487a1 fix: url interna
All checks were successful
Deploy QR Rapido / test (push) Successful in 26s
Deploy QR Rapido / build-and-push (push) Successful in 7m4s
Deploy QR Rapido / deploy-staging (push) Has been skipped
Deploy QR Rapido / deploy-production (push) Successful in 3m5s
2025-08-12 22:12:12 -03:00
Ricardo Carneiro
0fb14e6f14 fix: verificar se adicionar error a home dá certo
All checks were successful
Deploy QR Rapido / test (push) Successful in 26s
Deploy QR Rapido / build-and-push (push) Successful in 7m14s
Deploy QR Rapido / deploy-staging (push) Has been skipped
Deploy QR Rapido / deploy-production (push) Successful in 2m56s
2025-08-12 21:54:10 -03:00
Ricardo Carneiro
fc02c22f40 feat: error page
All checks were successful
Deploy QR Rapido / test (push) Successful in 3m32s
Deploy QR Rapido / build-and-push (push) Successful in 6m51s
Deploy QR Rapido / deploy-staging (push) Has been skipped
Deploy QR Rapido / deploy-production (push) Successful in 1m54s
2025-08-12 21:33:00 -03:00
Ricardo Carneiro
e8fad24cc8 fix: uri redirect para microsoft
All checks were successful
Deploy QR Rapido / test (push) Successful in 28s
Deploy QR Rapido / build-and-push (push) Successful in 6m51s
Deploy QR Rapido / deploy-staging (push) Has been skipped
Deploy QR Rapido / deploy-production (push) Successful in 1m56s
2025-08-12 21:03:15 -03:00
Ricardo Carneiro
4ed2d2243d fix: corrigir obter usuário
All checks were successful
Deploy QR Rapido / test (push) Successful in 29s
Deploy QR Rapido / build-and-push (push) Successful in 6m52s
Deploy QR Rapido / deploy-staging (push) Has been skipped
Deploy QR Rapido / deploy-production (push) Successful in 1m51s
2025-08-12 20:06:34 -03:00
Ricardo Carneiro
3e84e8d7a6 fix: ajustes de toast e msg de erro
All checks were successful
Deploy QR Rapido / test (push) Successful in 33s
Deploy QR Rapido / build-and-push (push) Successful in 6m59s
Deploy QR Rapido / deploy-staging (push) Has been skipped
Deploy QR Rapido / deploy-production (push) Successful in 1m59s
2025-08-12 18:23:56 -03:00
Ricardo Carneiro
83e46a5782 fix: sobre, contato, faq, etc. 2025-08-12 15:51:01 -03:00
Ricardo Carneiro
3feddbea11 fix: scroll to qrcode
All checks were successful
Deploy QR Rapido / test (push) Successful in 2m9s
Deploy QR Rapido / build-and-push (push) Successful in 13m5s
Deploy QR Rapido / deploy-staging (push) Has been skipped
Deploy QR Rapido / deploy-production (push) Successful in 1m50s
2025-08-11 20:35:57 -03:00
Ricardo Carneiro
33c6dc76c0 fix: 30 dias sem anuncios = sem limite diário
All checks were successful
Deploy QR Rapido / test (push) Successful in 45s
Deploy QR Rapido / build-and-push (push) Successful in 12m53s
Deploy QR Rapido / deploy-staging (push) Has been skipped
Deploy QR Rapido / deploy-production (push) Successful in 2m0s
2025-08-11 19:45:36 -03:00
Ricardo Carneiro
84612549ea fix: adsense
All checks were successful
Deploy QR Rapido / test (push) Successful in 33s
Deploy QR Rapido / build-and-push (push) Successful in 12m53s
Deploy QR Rapido / deploy-staging (push) Has been skipped
Deploy QR Rapido / deploy-production (push) Successful in 2m54s
2025-08-11 19:24:27 -03:00
Ricardo Carneiro
12afcb3d83 fix: adsense
Some checks failed
Deploy QR Rapido / test (push) Failing after 26s
Deploy QR Rapido / build-and-push (push) Has been skipped
Deploy QR Rapido / deploy-staging (push) Has been skipped
Deploy QR Rapido / deploy-production (push) Has been skipped
2025-08-11 19:20:39 -03:00
Ricardo Carneiro
bcf9f659b4 feat: adsense
All checks were successful
Deploy QR Rapido / test (push) Successful in 48s
Deploy QR Rapido / build-and-push (push) Successful in 13m55s
Deploy QR Rapido / deploy-staging (push) Has been skipped
Deploy QR Rapido / deploy-production (push) Successful in 1m53s
2025-08-11 12:23:01 -03:00
Ricardo Carneiro
e172969996 fix: ajustes pt-BR e es-PY 2025-08-10 14:45:53 -03:00
Ricardo Carneiro
988a0115f3 fix: appSettings
All checks were successful
Deploy QR Rapido / test (push) Successful in 27s
Deploy QR Rapido / build-and-push (push) Successful in 6m38s
Deploy QR Rapido / deploy-staging (push) Has been skipped
Deploy QR Rapido / deploy-production (push) Successful in 1m54s
2025-08-05 22:45:21 -03:00
Ricardo Carneiro
3395cc7478 fix: deploy com private key e appsettings correto
All checks were successful
Deploy QR Rapido / test (push) Successful in 28s
Deploy QR Rapido / build-and-push (push) Successful in 6m45s
Deploy QR Rapido / deploy-staging (push) Has been skipped
Deploy QR Rapido / deploy-production (push) Successful in 2m49s
2025-08-05 21:41:09 -03:00
Ricardo Carneiro
944de92af6 fix: deploy debug
Some checks failed
Deploy QR Rapido / test (push) Successful in 26s
Deploy QR Rapido / build-and-push (push) Has been cancelled
Deploy QR Rapido / deploy-staging (push) Has been cancelled
Deploy QR Rapido / deploy-production (push) Has been cancelled
2025-08-05 21:40:13 -03:00
Ricardo Carneiro
d608a9645c fix: ssh secret key
Some checks failed
Deploy QR Rapido / test (push) Successful in 30s
Deploy QR Rapido / build-and-push (push) Successful in 28s
Deploy QR Rapido / deploy-staging (push) Has been skipped
Deploy QR Rapido / deploy-production (push) Failing after 6s
2025-08-05 21:36:42 -03:00
Ricardo Carneiro
122b0e6f51 fix: deploy sem fingerprint
Some checks failed
Deploy QR Rapido / test (push) Successful in 26s
Deploy QR Rapido / build-and-push (push) Successful in 28s
Deploy QR Rapido / deploy-staging (push) Has been skipped
Deploy QR Rapido / deploy-production (push) Failing after 6s
2025-08-05 21:28:22 -03:00
Ricardo Carneiro
95c8e7901b feat: nem build
Some checks failed
Deploy QR Rapido / test (push) Successful in 27s
Deploy QR Rapido / build-and-push (push) Successful in 1m40s
Deploy QR Rapido / deploy-staging (push) Has been skipped
Deploy QR Rapido / deploy-production (push) Failing after 6s
2025-08-05 21:19:40 -03:00
Ricardo Carneiro
afd1b53354 feat: deploy to production
Some checks failed
Deploy QR Rapido / test (push) Successful in 1m5s
Deploy QR Rapido / build-and-push (push) Failing after 1m20s
Deploy QR Rapido / deploy-staging (push) Has been skipped
Deploy QR Rapido / deploy-production (push) Has been skipped
2025-08-05 21:13:21 -03:00
Ricardo Carneiro
c2c89af91b fix: msg ao excluir
Some checks failed
Deploy QR Rapido / test (push) Successful in 25s
Deploy QR Rapido / build-and-push (push) Failing after 4s
Deploy QR Rapido / deploy-staging (push) Has been skipped
Deploy QR Rapido / deploy-production (push) Has been skipped
2025-08-04 20:37:59 -03:00
Ricardo Carneiro
a7af34659b feat: delete e es-py
Some checks failed
Deploy QR Rapido / test (push) Successful in 29s
Deploy QR Rapido / build-and-push (push) Failing after 4s
Deploy QR Rapido / deploy-staging (push) Has been skipped
Deploy QR Rapido / deploy-production (push) Has been skipped
2025-08-04 20:34:29 -03:00
Ricardo Carneiro
70fbdaa3c2 feat: opacidade enquanto não selecionar o tipo.
Some checks failed
Deploy QR Rapido / test (push) Successful in 3m34s
Deploy QR Rapido / build-and-push (push) Failing after 8s
Deploy QR Rapido / deploy-staging (push) Has been skipped
Deploy QR Rapido / deploy-production (push) Has been skipped
2025-08-04 19:45:46 -03:00
Ricardo Carneiro
8ab0b913e2 fix: Melhorar a leitura mesmo com o logotipo maior.
Some checks failed
Deploy QR Rapido / test (push) Successful in 26s
Deploy QR Rapido / build-and-push (push) Failing after 5s
Deploy QR Rapido / deploy-staging (push) Has been skipped
Deploy QR Rapido / deploy-production (push) Has been skipped
2025-08-04 01:50:54 -03:00
Ricardo Carneiro
5f0d3dbf66 fix: logo cores e tamanho
Some checks failed
Deploy QR Rapido / test (push) Successful in 3m39s
Deploy QR Rapido / build-and-push (push) Failing after 5s
Deploy QR Rapido / deploy-staging (push) Has been skipped
Deploy QR Rapido / deploy-production (push) Has been skipped
2025-08-04 01:22:29 -03:00
Ricardo Carneiro
9b094ed712 fix: gerar qrcode com logo e com imagens de diferentes tamanhos. 2025-08-04 00:34:20 -03:00
Ricardo Carneiro
3fa95aefd8 feat: ajustes do stripe
Some checks failed
Deploy QR Rapido / test (push) Successful in 3m44s
Deploy QR Rapido / build-and-push (push) Failing after 6s
Deploy QR Rapido / deploy-staging (push) Has been skipped
Deploy QR Rapido / deploy-production (push) Has been skipped
2025-08-03 19:52:32 -03:00
Ricardo Carneiro
0c176a2abf feat: ajustes na seleção do tipo de qr code.
Some checks failed
Deploy QR Rapido / test (push) Successful in 27s
Deploy QR Rapido / build-and-push (push) Failing after 7s
Deploy QR Rapido / deploy-staging (push) Has been skipped
Deploy QR Rapido / deploy-production (push) Has been skipped
2025-08-01 16:35:52 -03:00
Ricardo Carneiro
286da5e5de fix: gerar url/link.
Some checks failed
Deploy QR Rapido / test (push) Successful in 32s
Deploy QR Rapido / build-and-push (push) Failing after 9s
Deploy QR Rapido / deploy-staging (push) Has been skipped
Deploy QR Rapido / deploy-production (push) Has been skipped
2025-08-01 10:33:34 -03:00
Ricardo Carneiro
ee160135af feat: todos os qr codes com os campos funcionais!!! (falta apenas o dinamico) 2025-07-31 23:59:45 -03:00
Ricardo Carneiro
9634176e18 feat: ajustes de focos dos campos.
Some checks failed
Deploy QR Rapido / test (push) Successful in 34s
Deploy QR Rapido / build-and-push (push) Failing after 10s
Deploy QR Rapido / deploy-staging (push) Has been skipped
Deploy QR Rapido / deploy-production (push) Has been skipped
2025-07-31 21:43:27 -03:00
177 changed files with 45222 additions and 5035 deletions

View File

@ -1,4 +1,5 @@
{
"$schema": "https://json.schemastore.org/claude-code-settings.json",
"permissions": {
"allow": [
"Bash(dotnet new:*)",
@ -9,8 +10,34 @@
"Bash(dotnet run:*)",
"Bash(curl:*)",
"Bash(pkill:*)",
"Bash(true)"
"Bash(true)",
"Bash(dotnet clean:*)",
"Bash(grep:*)",
"Bash(ss:*)",
"Bash(killall:*)",
"Bash(node:*)",
"Bash(jshint:*)",
"Bash(ls:*)",
"Bash(convert:*)",
"Bash(dotnet add package:*)",
"Bash(dotnet remove package:*)",
"Bash(dotnet restore:*)",
"Bash(rg:*)",
"Bash(dotnet test:*)",
"Bash(cp:*)",
"Bash(ping:*)",
"Bash(nc:*)",
"Bash(ssh:*)",
"Read(//mnt/c/vscode/**)",
"Read(//mnt/c/**)",
"Bash(chmod +x /mnt/c/vscode/qrrapido/Scripts/update-plans.sh)",
"Bash(netstat -tln)",
"Bash(npm install)",
"Bash(npm install:*)",
"Bash(git add:*)",
"Bash(git commit:*)",
"Bash(git push:*)"
],
"deny": []
}
}
}

54
.dockerignore Normal file
View File

@ -0,0 +1,54 @@
# Build output
bin/
obj/
out/
# IDE
.vs/
.vscode/
.idea/
*.user
*.suo
# Node
node_modules/
wwwroot/dist/
# Tests
Tests.E2E/
Tests/
coverage*/
TestResults/
# Git
.git/
.gitignore
.github/
.gitea/
# Dev configs (secrets never go in image)
appsettings.Local.json
appsettings.Production.json
*.Development.json
appsettings.Staging.json
secrets.env
scripts/secrets.env
keys/
.env*
# Logs / temp
logs/
uploads/
temp/
*.log
# Docs
*.md
!Content/
# Playwright
Tests.E2E/auth-state.json
Tests.E2E/last-api-key.txt
# macOS
.DS_Store

View File

@ -1,5 +1,4 @@
name: Deploy QR Rapido
on:
push:
branches: [ main, develop ]
@ -7,8 +6,8 @@ on:
branches: [ main ]
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
REGISTRY: registry.redecarneir.us
IMAGE_NAME: qrrapido
jobs:
test:
@ -31,13 +30,13 @@ jobs:
${{ runner.os }}-nuget-
- name: Restore dependencies
run: dotnet restore
run: dotnet restore QRRapidoApp.csproj
- name: Build
run: dotnet build --no-restore --configuration Release
run: dotnet build QRRapidoApp.csproj --no-restore --configuration Release
- name: Test
run: dotnet test --no-build --configuration Release --verbosity normal --collect:"XPlat Code Coverage"
run: dotnet test QRRapidoApp.csproj --no-build --configuration Release --verbosity normal --collect:"XPlat Code Coverage"
- name: Upload coverage
uses: codecov/codecov-action@v3
@ -46,57 +45,90 @@ jobs:
build-and-push:
needs: test
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
runs-on: [self-hosted, arm64, bcards]
if: github.ref == 'refs/heads/main' || github.ref == 'refs/heads/develop'
permissions:
contents: read
packages: write
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Login to Container Registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Build and push to registry
run: |
# Determina a tag baseada na branch
if [ "${{ github.ref }}" = "refs/heads/main" ]; then
TAG="latest"
else
TAG="develop"
fi
- name: Extract metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=ref,event=branch
type=ref,event=pr
type=sha,prefix={{branch}}-
type=raw,value=latest,enable={{is_default_branch}}
- name: Build and push
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
# Build da imagem para ARM64 (servidores Ampere OCI)
docker buildx build \
--platform linux/arm64 \
--tag ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:$TAG \
--push \
.
echo "IMAGE_TAG=$TAG" >> $GITHUB_ENV
deploy-staging:
needs: build-and-push
runs-on: ubuntu-latest
runs-on: [self-hosted, arm64, bcards]
if: github.ref == 'refs/heads/develop'
steps:
- name: Deploy to Staging
uses: azure/webapps-deploy@v2
with:
app-name: 'qrrapido-staging'
publish-profile: ${{ secrets.AZURE_WEBAPP_PUBLISH_PROFILE_STAGING }}
images: '${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:develop'
- name: Deploy to Staging Servers
run: |
# Configura SSH
mkdir -p ~/.ssh
echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/id_rsa
chmod 600 ~/.ssh/id_rsa
# Deploy no Servidor 1
ssh -o StrictHostKeyChecking=no ubuntu@141.148.162.114 << 'EOF'
# Para o container atual se existir
docker stop qrrapido-staging || true
docker rm qrrapido-staging || true
# Remove imagem antiga
docker rmi ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:develop || true
# Puxa nova imagem
docker pull ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:develop
# Executa novo container
docker run -d \
--name qrrapido-staging \
--restart unless-stopped \
-p 5000:8080 \
--add-host=host.docker.internal:host-gateway \
-e ASPNETCORE_ENVIRONMENT=Staging \
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:develop
EOF
# Deploy no Servidor 2
ssh -o StrictHostKeyChecking=no ubuntu@129.146.116.218 << 'EOF'
# Para o container atual se existir
docker stop qrrapido-staging || true
docker rm qrrapido-staging || true
# Remove imagem antiga
docker rmi ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:develop || true
# Puxa nova imagem
docker pull ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:develop
# Executa novo container
docker run -d \
--name qrrapido-staging \
--restart unless-stopped \
-p 5000:8080 \
--add-host=host.docker.internal:host-gateway \
-e ASPNETCORE_ENVIRONMENT=Staging \
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:develop
EOF
deploy-production:
needs: build-and-push
@ -105,17 +137,118 @@ jobs:
environment: production
steps:
- name: Deploy to Production
uses: azure/webapps-deploy@v2
with:
app-name: 'qrrapido-production'
publish-profile: ${{ secrets.AZURE_WEBAPP_PUBLISH_PROFILE_PROD }}
images: '${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest'
- name: Deploy to Production Swarm
run: |
# Debug SSH setup
echo "=== Configurando SSH ==="
mkdir -p ~/.ssh
echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/id_rsa
chmod 600 ~/.ssh/id_rsa
# Verifica se a chave foi criada
echo "=== Verificando chave SSH ==="
ls -la ~/.ssh/
echo "Primeiras linhas da chave:"
head -2 ~/.ssh/id_rsa
# Testa conexão SSH com debug
echo "=== Testando conexão SSH ==="
ssh -o StrictHostKeyChecking=no -o ConnectTimeout=10 -v ubuntu@141.148.162.114 'echo "Conexão SSH funcionando!"' || echo "Falha na conexão SSH"
# Se a conexão funcionou, continua com o deploy
echo "=== Iniciando Deploy no Docker Swarm ==="
# Deploy via Docker Swarm with Secrets
ssh -o StrictHostKeyChecking=no ubuntu@141.148.162.114 << 'EOF'
# Puxa nova imagem
docker pull ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
# Cria o diretório de chaves no host e define as permissões corretas
sudo mkdir -p /app/keys
sudo chown -R 1000:1000 /app/keys
# Cria a rede overlay se não existir
docker network create --driver overlay --attachable qrrapido-network || echo "Rede já existe"
# Verifica se os secrets existem (devem ser criados manualmente uma vez)
echo "Verificando Docker Secrets..."
docker secret ls | grep -q "stripe_secret_key" || echo "AVISO: Secret stripe_secret_key não existe!"
docker secret ls | grep -q "stripe_webhook_secret" || echo "AVISO: Secret stripe_webhook_secret não existe!"
docker secret ls | grep -q "mongodb_connection_string" || echo "AVISO: Secret mongodb_connection_string não existe!"
docker secret ls | grep -q "google_client_id" || echo "AVISO: Secret google_client_id não existe!"
docker secret ls | grep -q "google_client_secret" || echo "AVISO: Secret google_client_secret não existe!"
docker secret ls | grep -q "microsoft_client_id" || echo "AVISO: Secret microsoft_client_id não existe!"
docker secret ls | grep -q "microsoft_client_secret" || echo "AVISO: Secret microsoft_client_secret não existe!"
# Verifica se o service existe
if docker service inspect qrrapido-prod > /dev/null 2>&1; then
echo "Service existe, atualizando imagem..."
docker service update \
--image ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest \
--env-add Serilog__OpenSearchUrl="http://141.148.162.114:19201" \
--env-add Serilog__OpenSearchFallback="http://129.146.116.218:19202" \
--env-add Admin__AllowedEmails__0="rrcgoncalves@gmail.com" \
--update-order start-first \
--update-delay 30s \
--update-parallelism 1 \
--with-registry-auth \
qrrapido-prod
else
echo "Service não existe, criando com secrets..."
docker service create \
--name qrrapido-prod \
--replicas 2 \
--network qrrapido-network \
--publish published=5001,target=8080 \
--mount type=bind,source=/app/keys,target=/app/keys \
--secret stripe_secret_key \
--secret stripe_webhook_secret \
--secret mongodb_connection_string \
--secret google_client_id \
--secret google_client_secret \
--secret microsoft_client_id \
--secret microsoft_client_secret \
--env ASPNETCORE_ENVIRONMENT=Production \
--env ASPNETCORE_URLS=http://+:8080 \
--env Serilog__OpenSearchUrl="http://141.148.162.114:19201" \
--env Serilog__OpenSearchFallback="http://129.146.116.218:19202" \
--env Admin__AllowedEmails__0="rrcgoncalves@gmail.com" \
--update-delay 30s \
--update-parallelism 1 \
--update-order start-first \
--restart-condition on-failure \
--restart-max-attempts 3 \
--with-registry-auth \
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
fi
# Aguarda o service estar estável
echo "Aguardando service estabilizar..."
sleep 30
# Verifica se o service está funcionando
docker service ps qrrapido-prod
# Recarrega NGINX para garantir que está apontando para o novo container
sudo nginx -t && sudo systemctl reload nginx
EOF
- name: Notify Slack
uses: 8398a7/action-slack@v3
with:
status: ${{ job.status }}
channel: '#deployments'
webhook_url: ${{ secrets.SLACK_WEBHOOK }}
text: "QR Rapido deployed to production! 🚀"
- name: Health Check Swarm
run: |
# Aguarda um pouco para o service estabilizar
sleep 15
# Verifica o status do service no swarm
echo "Verificando status do service no Swarm..."
ssh -o StrictHostKeyChecking=no ubuntu@141.148.162.114 'docker service ps qrrapido-prod'
# Verifica se os serviços estão respondendo em ambos servidores
echo "Verificando Servidor 1..."
ssh -o StrictHostKeyChecking=no ubuntu@141.148.162.114 'curl -f http://localhost:5001/healthcheck || echo "⚠️ Servidor 1 pode não estar respondendo"'
echo "Verificando Servidor 2..."
ssh -o StrictHostKeyChecking=no ubuntu@129.146.116.218 'curl -f http://localhost:5001/healthcheck || echo "⚠️ Servidor 2 pode não estar respondendo"'
# Testa o site principal
echo "Testando site principal..."
curl -f https://qrrapido.site || echo "⚠️ Site principal pode não estar respondendo"

11
.gitignore vendored
View File

@ -1,3 +1,7 @@
# Playwright E2E — sessão salva e chaves temporárias (contêm cookies/secrets)
Tests.E2E/auth-state.json
Tests.E2E/last-api-key.txt
## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons.
##
@ -352,6 +356,7 @@ MigrationBackup/
FodyWeavers.xsd
# Custom
keys/
appsettings.Local.json
appsettings.Production.json
*.Development.json
@ -361,6 +366,8 @@ temp/
*.env
.env.local
.env.production
secrets.env
scripts/secrets.env
# macOS
.DS_Store
@ -379,4 +386,6 @@ temp/
# Certificates
*.pfx
*.crt
*.key
*.key
wwwroot/dist/
.aider*

19
AGENTS.md Normal file
View File

@ -0,0 +1,19 @@
# Repository Guidelines
## Project Structure & Module Organization
Core MVC code lives in `Controllers`, `Services`, `Models`, and `Middleware`, while Razor views sit in `Views` and static assets in `wwwroot`. Data access and caching helpers are under `Data` and `Providers`. Localized resources (PT-BR, ES, EN) reside in `Resources`. Tests target service logic in `Tests/Services` via `QRRapidoApp.Tests.csproj`. Runtime settings use the `appsettings.*.json` files, and Docker assets, including `docker-compose.yml`, remain at the repository root.
## Build, Test, and Development Commands
Install dependencies with `dotnet restore`, then run `dotnet build` for a release-ready compile. Use `dotnet run` to launch the Kestrel server locally or `dotnet watch run` for hot reload during UI work. Execute `dotnet test` from the root to run xUnit tests; add `--collect:"XPlat Code Coverage"` when you need coverage reports. For full-stack parity, start infrastructure with `docker-compose up -d` and follow component logs through `docker-compose logs -f qrrapido`.
## Coding Style & Naming Conventions
Stick to standard C# styling: four-space indentation, PascalCase for types and public members, camelCase for locals, and `I` prefixes for interfaces. Keep services focused; prefer small, testable classes instead of partials. Place shared copy in `Resources` and surface it via `IStringLocalizer`. Run `dotnet format` before committing to normalize imports, analyzer fixes, and whitespace.
## Testing Guidelines
Keep unit and integration tests close to the subject, e.g., `Tests/Services/QRRapidoServiceTests.cs`. Name classes `<Subject>Tests` and methods as scenario sentences such as `GenerateRapidAsync_WithValidRequest_ReturnsSuccess`. Use Moq for external dependencies and configure defaults in constructor helpers. Every new behavior should gain at least one happy-path and one guard test, and maintain quick, deterministic fixtures.
## Commit & Pull Request Guidelines
Follow the Conventional Commit style observed here (`fix:`, `feat:`, `chore:`) with concise Portuguese summaries for user-facing updates. Group related changes per commit to simplify reversions. Pull requests should outline the context, enumerate key changes, attach `dotnet test` output or coverage deltas, and include UI screenshots or GIFs when behavior shifts. Reference Azure or GitHub issue IDs and call out configuration or migration steps prominently.
## Security & Configuration Tips
Never commit secrets or the contents of `keys/`. Document new configuration values in the PR description and supply safe defaults in `appsettings.Development.json`. Protect added endpoints with existing rate-limiting and authorization middleware, and rotate published test keys after demos.

BIN
AoSelecionarES.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 434 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 449 KiB

BIN
AoSelecionarWifi.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 432 KiB

286
CLAUDE.md Normal file
View File

@ -0,0 +1,286 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project Overview
QR Rapido is an ultra-fast QR code generator built with ASP.NET Core 8.0, focusing on speed and multilingual support (PT-BR, ES, EN). It features tiered user access (anonymous, logged-in free, premium), OAuth authentication (Google/Microsoft), Stripe payment integration, and ad-free session management.
**Key Performance Targets:**
- QR generation: <1.2s (average), <0.4s (premium users)
- Cache hit rate: >80%
- First Contentful Paint: <2s
## Common Commands
### Development
```bash
# Run locally (hot reload)
dotnet watch run
# Run locally (standard)
dotnet run
# Build for release
dotnet build --configuration Release
# Restore dependencies
dotnet restore
```
### Testing
```bash
# Run all tests
dotnet test
# Run with coverage
dotnet test --collect:"XPlat Code Coverage"
# Run specific test class
dotnet test --filter "QRRapidoServiceTests"
```
### Frontend Build
```bash
# Development mode with Vite
npm run dev
# Production build (Vite)
npm run build
# Preview production build
npm run preview
```
### Docker
```bash
# Build and run all services (MongoDB, Redis, Nginx)
docker-compose up -d
# View logs
docker-compose logs -f qrrapido
# Build production image for ARM64 (OCI Ampere servers)
docker buildx build --platform linux/arm64 -t qrrapido:latest .
# Stop all services
docker-compose down
```
### Stripe Webhooks (Local Testing - WSL)
```bash
# Forward Stripe webhooks to local HTTPS endpoint
stripe listen --forward-to https://localhost:52428/pagamento/stripewebhook --skip-verify
```
## Architecture
### Core Technology Stack
- **Backend**: ASP.NET Core 8.0 (MVC + Razor Pages)
- **Database**: MongoDB (users, QR history, sessions)
- **Cache**: Redis (distributed cache) + MemoryCache (fallback)
- **QR Generation**: QRCoder library with ImageSharp for logo overlay
- **Authentication**: Cookie-based with OAuth (Google, Microsoft)
- **Payments**: Stripe (subscriptions)
- **Logging**: Serilog → OpenSearch/Console
- **Frontend Build**: Vite for asset bundling
### Project Structure
```
Controllers/ # MVC controllers (Home, QR, Account, Premium, Pagamento, Health)
Services/ # Business logic (QRRapido, User, Plan, Stripe, AdDisplay)
├── Monitoring/ # Resource and MongoDB monitoring
└── HealthChecks/# Health check implementations
Models/ # Domain models and ViewModels
Data/ # MongoDbContext
Middleware/ # Custom middleware (Language redirection, LastLogin update)
Providers/ # Culture providers for localization
Resources/ # .resx files for PT-BR, ES, EN localization
Views/ # Razor views
wwwroot/ # Static assets (CSS, JS, images)
Tests/ # Unit tests (xUnit, Moq)
```
### Key Services
**QRRapidoService** (`Services/QRRapidoService.cs`):
- Core QR generation with distributed cache support
- SemaphoreSlim limits concurrent generations (default: 100)
- Cache key based on content hash + settings
- Optimized error correction levels for speed
- Logo overlay with readability analysis
**UserService** (`Services/UserService.cs`):
- MongoDB user CRUD operations
- QR history management (anonymous vs authenticated)
- Premium status checks
- Graceful fallback when MongoDB unavailable
**AdDisplayService** (`Services/AdDisplayService.cs`):
- Controls ad visibility based on user status
- 30-day ad-free period after login
- Premium users: permanent ad-free
- Anonymous users: always show ads
**StripeService** (`Services/StripeService.cs`):
- Subscription creation and management
- Webhook handling (checkout.session.completed, etc.)
- Customer portal session creation
### Middleware Pipeline (Program.cs)
1. **LanguageRedirectionMiddleware**: Redirects root `/` to `/pt-BR/` or user's language preference
2. **Request Localization**: Sets culture based on route (`/{culture}/...`) → QueryString → Cookie
3. **Authentication/Authorization**
4. **Session**
5. **LastLoginUpdateMiddleware**: Updates user's last login timestamp
### Localization Strategy
- Route-based culture: `/{culture:regex(^(pt-BR|es-PY)$)}/{controller}/{action}`
- Default culture: `pt-BR`
- Supported cultures: `pt-BR`, `es-PY`
- Culture providers priority: Route → QueryString → Cookie
- Resources in `Resources/SharedResource.{culture}.resx`
### MongoDB Collections
- **Users**: User profiles, premium status, OAuth data
- **QRCodeHistory**: Generated QR codes (linked to users or anonymous)
- **AdFreeSessions**: Ad-free session tracking (30-day grants)
- **DataProtectionKeys**: ASP.NET Core Data Protection keys (for Docker Swarm)
### Docker Swarm Deployment
**Production** uses Docker Swarm with:
- 2 replicas across 2 ARM64 servers (OCI Ampere)
- Shared MongoDB for Data Protection keys (cross-replica cookie decryption)
- Rolling updates: `start-first` strategy, 30s delay between updates
- Health checks at `/healthcheck` endpoint
- Exposed on port 5001 internally, proxied by Nginx
**Staging** uses standalone Docker containers on 2 servers.
### CI/CD Pipeline (.github/workflows/deploy.yml)
1. **Test Job**: Runs on `ubuntu-latest`
- Restore → Build → Test with coverage
- Uploads coverage to Codecov
2. **Build-and-Push Job**: Runs on self-hosted ARM64 runner
- Builds ARM64 Docker image
- Tags: `latest` (main), `develop` (develop branch)
- Pushes to private registry: `registry.redecarneir.us`
3. **Deploy-Staging**: SSH to 2 servers, pull image, run container
4. **Deploy-Production**: SSH to Swarm manager, update service with zero-downtime
### Configuration Management
**appsettings.json** key sections:
- `ConnectionStrings:MongoDB`: MongoDB connection string
- `Authentication:Google/Microsoft`: OAuth credentials
- `Stripe`: API keys and webhook secret
- `Performance:MaxConcurrentGenerations`: Semaphore limit (default: 100)
- `Premium:FreeQRLimit`: Daily limit for logged-in free users (10)
- `Serilog:OpenSearchUrl`: Centralized logging endpoint
- `ResourceMonitoring`: CPU/memory thresholds for alerts
- `HealthChecks`: Timeout and test configurations
**Environment-specific overrides**:
- `appsettings.Development.json`
- `appsettings.Production.json`
### Rate Limiting & Performance
- Fixed window rate limiter: 600 requests/minute per IP on `/api` endpoints
- Kestrel max connections: 2000
- QR generation timeout: 2000ms
- Redis cache expiration: 60 minutes
- MongoDB query timeouts: 5 seconds (health checks)
### Health Checks
Endpoint: `/healthcheck`
Checks:
- **MongoDbHealthCheck**: Database connectivity, size metrics, test query
- **ResourceHealthCheck**: CPU/memory usage, GC pressure
- **ExternalServicesHealthCheck**: Stripe API availability
### Testing Strategy
- **Unit tests**: Services layer (QRRapidoService, AdDisplayService, etc.)
- **Mocking**: MongoDB and IDistributedCache with Moq
- **Coverage**: Run `dotnet test --collect:"XPlat Code Coverage"`
- Test files in `Tests/Services/`
### Known Quirks & WSL Compatibility
- **StaticWebAssets disabled**: Set `ASPNETCORE_HOSTINGSTARTUP__STATICWEBASSETS__ENABLED=false` for WSL path issues (see Program.cs:38-39)
- **DataProtection**: Uses MongoDB for key persistence in production (Docker Swarm), filesystem in development
- **Frontend build**: Vite runs during Release build via MSBuild target (`BuildFrontend` in .csproj)
- **Stripe local testing**: Use `stripe listen` in WSL (see README.md:345-347)
### Monitoring & Logging
- **Serilog** → Console (development) + OpenSearch (production)
- **ResourceMonitoringService**: Background service tracking CPU/memory every 30s
- **HistoryCleanupService**: Cleans old anonymous QR history (7-day grace period, runs every 6 hours)
- **MongoDbMonitoringService**: Tracks database growth and collection stats (disabled by default)
### Premium Feature Gates
Check user premium status via `IUserService.GetUserAsync(userId)``user.IsPremium`
Premium benefits:
- Unlimited QR codes (vs 10/day anonymous, 50/day free)
- No ads permanently
- Priority generation (faster SemaphoreSlim release)
- Dynamic QR codes (editable)
- API access
### Ad-Free Logic
See `AdDisplayService.ShouldShowAdsAsync()`:
- Anonymous users: always show ads
- Premium users: never show ads
- Free logged-in users: 30-day ad-free period from login (configurable in appsettings)
- Ad-free sessions tracked in MongoDB `AdFreeSessions` collection
### Security Considerations
- OAuth secure flow with PKCE
- HTTPS redirect enforced (non-dev environments)
- Stripe webhook signature verification
- Input sanitization on QR generation
- Rate limiting on API endpoints
- HSTS enabled in production
- Forwarded headers support for reverse proxy (Nginx)
### Common Workflows
**Adding a new QR type:**
1. Update `Models/ViewModels/QRGenerationRequest.cs` with new type enum
2. Add generation logic in `Services/QRRapidoService.cs``GenerateQRCodeOptimizedAsync()`
3. Update frontend form in `Views/Home/Index.cshtml`
4. Add localized strings in `Resources/SharedResource.{culture}.resx`
**Adding a new language:**
1. Create `Resources/SharedResource.{culture}.resx`
2. Update `Program.cs` supported cultures array (line 214-218)
3. Update route constraint regex (line 354)
4. Add culture provider mapping if needed
**Debugging slow QR generation:**
1. Check `_logger` output in `QRRapidoService.GenerateRapidAsync()` for timing
2. Verify cache hit rate in logs
3. Check semaphore wait time (max concurrent limit)
4. Review `Performance:QRGenerationTimeoutMs` in appsettings
5. Monitor resource usage via `/healthcheck`
**Updating Stripe configuration:**
1. Update `appsettings.json``Stripe` section
2. Verify webhook secret matches Stripe dashboard
3. Test locally with `stripe listen --forward-to ...`
4. Update `StripeService.cs` webhook handlers if event types change

View File

@ -0,0 +1,111 @@
using Microsoft.Extensions.Configuration;
namespace QRRapidoApp.Configuration
{
/// <summary>
/// Configuration provider that reads Docker Secrets from /run/secrets/
/// Secrets are mounted as files in Swarm mode.
/// </summary>
public class DockerSecretsConfigurationProvider : ConfigurationProvider
{
private readonly string _secretsPath;
private readonly Dictionary<string, string> _secretKeyMappings;
public DockerSecretsConfigurationProvider(string secretsPath, Dictionary<string, string> secretKeyMappings)
{
_secretsPath = secretsPath;
_secretKeyMappings = secretKeyMappings;
}
public override void Load()
{
Data = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
if (!Directory.Exists(_secretsPath))
{
return;
}
var secretsLoaded = 0;
foreach (var mapping in _secretKeyMappings)
{
var secretFileName = mapping.Key;
var configKey = mapping.Value;
var secretFilePath = Path.Combine(_secretsPath, secretFileName);
if (File.Exists(secretFilePath))
{
try
{
var secretValue = (string?)File.ReadAllText(secretFilePath).Trim();
if (!string.IsNullOrEmpty(secretValue))
{
Data[configKey] = secretValue;
secretsLoaded++;
}
}
catch (Exception)
{
// Silently ignore read errors - will fall back to other config sources
}
}
}
// Set indicator that secrets were loaded from Docker
if (secretsLoaded > 0)
{
Data["App:SecretsLoaded"] = "true";
}
}
}
public class DockerSecretsConfigurationSource : IConfigurationSource
{
public string SecretsPath { get; set; } = "/run/secrets";
public Dictionary<string, string> SecretKeyMappings { get; set; } = new();
public IConfigurationProvider Build(IConfigurationBuilder builder)
{
return new DockerSecretsConfigurationProvider(SecretsPath, SecretKeyMappings);
}
}
public static class DockerSecretsConfigurationExtensions
{
/// <summary>
/// Adds Docker Secrets as a configuration source.
/// Maps secret file names to configuration keys.
/// </summary>
public static IConfigurationBuilder AddDockerSecrets(
this IConfigurationBuilder builder,
Action<DockerSecretsConfigurationSource>? configure = null)
{
var source = new DockerSecretsConfigurationSource
{
// Default mappings for QRRapido secrets
SecretKeyMappings = new Dictionary<string, string>
{
// Stripe
["stripe_secret_key"] = "Stripe:SecretKey",
["stripe_webhook_secret"] = "Stripe:WebhookSecret",
// MongoDB
["mongodb_connection_string"] = "ConnectionStrings:MongoDB",
// OAuth - Google
["google_client_id"] = "Authentication:Google:ClientId",
["google_client_secret"] = "Authentication:Google:ClientSecret",
// OAuth - Microsoft
["microsoft_client_id"] = "Authentication:Microsoft:ClientId",
["microsoft_client_secret"] = "Authentication:Microsoft:ClientSecret",
}
};
configure?.Invoke(source);
builder.Add(source);
return builder;
}
}
}

View File

@ -0,0 +1,129 @@
---
title: "PNG, WebP or SVG? Choosing the Right Format for Your QR Code"
description: "Technical comparison between PNG, WebP and SVG for QR codes via API. Learn which format to use depending on your use case: email, printing, web or apps."
keywords: "qr code png, qr code webp, qr code svg, qr code image format, api qr code format"
author: "QRRapido"
date: 2026-03-08
lastmod: 2026-03-08
image: ""
---
# PNG, WebP or SVG? Choosing the Right Format for Your QR Code
The QRRapido API supports three output formats: `png`, `webp` and `svg`. Each has distinct characteristics that impact file size, quality and compatibility. Use the `outputFormat` parameter in the request to choose.
```json
{
"content": "https://yoursite.com",
"type": "url",
"outputFormat": "webp"
}
```
---
## Quick Summary
| Format | Size | Lossless scaling | Compatibility | Ideal for |
|---|---|---|---|---|
| **PNG** | Medium | No (raster) | Universal | Printing, email, legacy |
| **WebP** | Small (~30% smaller) | No (raster) | Modern browsers | Web, apps, APIs |
| **SVG** | Variable (usually smaller) | **Yes** | Browsers, Adobe, Figma | Logos, banners, vector printing |
---
## PNG — The Universal Standard
PNG is a lossless raster format, ideal for QR codes because it preserves 100% of the black and white pixels without introducing artifacts.
**When to use:**
- Sending by email (legacy email clients don't support WebP)
- Integration with legacy systems
- When maximum compatibility is a priority
**Why not JPEG?**
> JPEG uses lossy compression that introduces blur artifacts on the edges of QR code modules. This can make the code unreadable, especially at smaller sizes. **Never use JPEG for QR codes.**
```json
{ "outputFormat": "png" }
```
---
## WebP — Recommended for Web and Apps
WebP is the ideal format for most modern integrations. Visual quality is indistinguishable from PNG for QR codes, with **2535% smaller file size**.
**Advantages:**
- Faster loading on web pages
- Lower bandwidth usage in APIs with high volume
- Native support in all modern browsers (Chrome, Safari 14+, Firefox, Edge)
- Lower storage consumption in buckets (S3, GCS, etc.)
**When to use:**
- Display in `<img>` on websites and web applications
- iOS/Android apps (both support WebP)
- When you store the generated QR codes
```json
{ "outputFormat": "webp" }
```
To display in HTML:
```html
<picture>
<source srcset="data:image/webp;base64,{{ qrCodeBase64 }}" type="image/webp">
<img src="data:image/png;base64,{{ fallbackBase64 }}" alt="QR Code">
</picture>
```
---
## SVG — For Lossless Scaling
SVG is a vector format: the QR code is described mathematically, not as pixels. This means it can be resized to any size — from a 16px icon to a 3-meter banner — without loss of quality.
**When to use:**
- Graphic materials (banners, posters, packaging, merchandise)
- Integration with design tools (Figma, Illustrator, Canva)
- High-quality printing without depending on resolution
**Caution:**
- Do not use SVG for email sending (most email clients block SVG for security reasons)
- Some older QR readers may have difficulty with SVG rendered directly via `<img>`
```json
{ "outputFormat": "svg" }
```
To display SVG inline, the `qrCodeBase64` must be decoded first:
```js
const svgString = atob(qrCodeBase64);
document.getElementById('qr').innerHTML = svgString;
```
---
## Size Comparison (real example — 400px, simple URL)
| Format | Typical size |
|---|---|
| PNG | ~812 KB |
| WebP | ~58 KB |
| SVG | ~36 KB |
> Sizes vary depending on content complexity (QR codes with more data have more modules).
---
## Recommendation by Use Case
| Scenario | Recommended format |
|---|---|
| Display on website/app | **WebP** |
| Send by email | **PNG** |
| Graphic printing / design | **SVG** |
| Store in cloud | **WebP** |
| Maximum compatibility | **PNG** |
| No concern about size | **PNG** |

View File

@ -0,0 +1,129 @@
---
title: "PNG, WebP o SVG: Eligiendo el Formato Correcto para tu Código QR"
description: "Comparativa técnica entre PNG, WebP y SVG para códigos QR vía API. Sabé cuál formato usar según tu caso de uso: e-mail, impresión, web o apps."
keywords: "qr code png, qr code webp, qr code svg, formato imagen qr code, api qr code formato paraguay"
author: "QRRapido"
date: 2026-03-08
lastmod: 2026-03-08
image: ""
---
# PNG, WebP o SVG: Eligiendo el Formato Correcto para tu Código QR
La API QRRapido soporta tres formatos de salida: `png`, `webp` ha `svg`. Cada uno tiene características distintas que impactan el tamaño del archivo, la calidad ha la compatibilidad. Usá el parámetro `outputFormat` en la solicitud para elegir.
```json
{
"content": "https://tuempresa.com.py",
"type": "url",
"outputFormat": "webp"
}
```
---
## Resumen Rápido
| Formato | Tamaño | Escala sin pérdida | Compatibilidad | Ideal para |
|---|---|---|---|---|
| **PNG** | Medio | No (raster) | Universal | Impresión, e-mail, sistemas legados |
| **WebP** | Pequeño (~30% menor) | No (raster) | Browsers modernos | Web, apps, APIs |
| **SVG** | Variable (generalmente menor) | **Sí** | Browsers, Adobe, Figma | Logos, banners, impresión vectorial |
---
## PNG — El Estándar Universal
PNG es un formato raster sin pérdida, ideal para códigos QR porque preserva 100% de los píxeles blancos ha negros sin introducir artefactos.
**Cuándo usarlo:**
- Envío por e-mail (clientes de correo antiguos no soportan WebP)
- Integración con sistemas legados
- Cuando la compatibilidad máxima es prioridad
**¿Por qué no JPEG?**
> JPEG usa compresión con pérdida que introduce artefactos de borrosidad en los bordes de los módulos del código QR. Eso puede volverlo ilegible, especialmente en tamaños pequeños. **Nunca usés JPEG para códigos QR.**
```json
{ "outputFormat": "png" }
```
---
## WebP — Recomendado para Web ha Apps
WebP es el formato porã para la mayoría de las integraciones modernas. La calidad visual es indistinguible del PNG para códigos QR, con **2535% menos tamaño de archivo**.
**Ventajas:**
- Carga más rápida en páginas web
- Menor uso de ancho de banda en APIs con alto volumen
- Soporte nativo en todos los browsers modernos (Chrome, Safari 14+, Firefox, Edge)
- Menor consumo de almacenamiento en la nube (S3, GCS, etc.)
**Cuándo usarlo:**
- Visualización en `<img>` en sitios ha aplicaciones web
- Apps iOS/Android (ambos soportan WebP)
- Cuando almacenás los QR generados
```json
{ "outputFormat": "webp" }
```
Para mostrar en HTML:
```html
<picture>
<source srcset="data:image/webp;base64,{{ qrCodeBase64 }}" type="image/webp">
<img src="data:image/png;base64,{{ fallbackBase64 }}" alt="Código QR">
</picture>
```
---
## SVG — Para Escalar sin Pérdida
SVG es un formato vectorial: el código QR se describe matemáticamente, no como píxeles. Eso significa que se puede redimensionar a cualquier tamaño — desde un ícono de 16px hasta un banner de 3 metros — sin perder calidad.
**Cuándo usarlo:**
- Material gráfico (banners, carteles, packaging, remeras)
- Integración con herramientas de diseño (Figma, Illustrator, Canva)
- Impresión de alta calidad sin depender de la resolución
**Atención:**
- No usés SVG para envío por e-mail (la mayoría de los clientes bloquea SVG por seguridad)
- Algunos lectores de QR antiguos pueden tener dificultad con SVG renderizado directamente vía `<img>`
```json
{ "outputFormat": "svg" }
```
Para mostrar SVG inline, el `qrCodeBase64` debe decodificarse primero:
```js
const svgString = atob(qrCodeBase64);
document.getElementById('qr').innerHTML = svgString;
```
---
## Comparativa de Tamaño (ejemplo real — 400px, URL simple)
| Formato | Tamaño típico |
|---|---|
| PNG | ~812 KB |
| WebP | ~58 KB |
| SVG | ~36 KB |
> Los tamaños varían según la complejidad del contenido (los QR con más datos tienen más módulos).
---
## Recomendación por Caso de Uso
| Escenario | Formato recomendado |
|---|---|
| Mostrar en sitio/app | **WebP** |
| Enviar por e-mail | **PNG** |
| Impresión gráfica / diseño | **SVG** |
| Guardar en la nube | **WebP** |
| Máxima compatibilidad | **PNG** |
| Sin preocupación por tamaño | **PNG** |

View File

@ -0,0 +1,129 @@
---
title: "PNG, WebP o SVG: Eligiendo el Formato Correcto para tu Código QR"
description: "Comparativa técnica entre PNG, WebP y SVG para códigos QR vía API. Conoce cuál formato usar según tu caso de uso: e-mail, impresión, web o apps."
keywords: "qr code png, qr code webp, qr code svg, formato imagen qr code, api qr code formato"
author: "QRRapido"
date: 2026-03-08
lastmod: 2026-03-08
image: ""
---
# PNG, WebP o SVG: Eligiendo el Formato Correcto para tu Código QR
La API QRRapido soporta tres formatos de salida: `png`, `webp` y `svg`. Cada uno tiene características distintas que impactan el tamaño del archivo, la calidad y la compatibilidad. Usa el parámetro `outputFormat` en la solicitud para elegir.
```json
{
"content": "https://tuempresa.com",
"type": "url",
"outputFormat": "webp"
}
```
---
## Resumen Rápido
| Formato | Tamaño | Escala sin pérdida | Compatibilidad | Ideal para |
|---|---|---|---|---|
| **PNG** | Medio | No (raster) | Universal | Impresión, e-mail, sistemas legados |
| **WebP** | Pequeño (~30% menor) | No (raster) | Browsers modernos | Web, apps, APIs |
| **SVG** | Variable (generalmente menor) | **Sí** | Browsers, Adobe, Figma | Logos, banners, impresión vectorial |
---
## PNG — El Estándar Universal
PNG es un formato raster sin pérdida, ideal para códigos QR porque preserva 100% de los píxeles blancos y negros sin introducir artefactos.
**Cuándo usarlo:**
- Envío por e-mail (clientes de correo antiguos no soportan WebP)
- Integración con sistemas legados
- Cuando la compatibilidad máxima es prioridad
**¿Por qué no JPEG?**
> JPEG usa compresión con pérdida que introduce artefactos de borrosidad en los bordes de los módulos del código QR. Eso puede volverlo ilegible, especialmente en tamaños pequeños. **Nunca uses JPEG para códigos QR.**
```json
{ "outputFormat": "png" }
```
---
## WebP — Recomendado para Web y Apps
WebP es el formato ideal para la mayoría de las integraciones modernas. La calidad visual es indistinguible del PNG para códigos QR, con **2535% menos tamaño de archivo**.
**Ventajas:**
- Carga más rápida en páginas web
- Menor uso de ancho de banda en APIs con alto volumen
- Soporte nativo en todos los browsers modernos (Chrome, Safari 14+, Firefox, Edge)
- Menor consumo de almacenamiento en la nube (S3, GCS, etc.)
**Cuándo usarlo:**
- Visualización en `<img>` en sitios y aplicaciones web
- Apps iOS/Android (ambos soportan WebP)
- Cuando guardas los QR generados
```json
{ "outputFormat": "webp" }
```
Para mostrar en HTML:
```html
<picture>
<source srcset="data:image/webp;base64,{{ qrCodeBase64 }}" type="image/webp">
<img src="data:image/png;base64,{{ fallbackBase64 }}" alt="Código QR">
</picture>
```
---
## SVG — Para Escalar sin Pérdida
SVG es un formato vectorial: el código QR se describe matemáticamente, no como píxeles. Eso significa que se puede redimensionar a cualquier tamaño — desde un ícono de 16px hasta un banner de 3 metros — sin perder calidad.
**Cuándo usarlo:**
- Material gráfico (banners, carteles, packaging, camisetas)
- Integración con herramientas de diseño (Figma, Illustrator, Canva)
- Impresión de alta calidad sin depender de la resolución
**Atención:**
- No uses SVG para envío por e-mail (la mayoría de los clientes bloquea SVG por seguridad)
- Algunos lectores de QR antiguos pueden tener dificultad con SVG renderizado directamente vía `<img>`
```json
{ "outputFormat": "svg" }
```
Para mostrar SVG inline, el `qrCodeBase64` debe decodificarse primero:
```js
const svgString = atob(qrCodeBase64);
document.getElementById('qr').innerHTML = svgString;
```
---
## Comparativa de Tamaño (ejemplo real — 400px, URL simple)
| Formato | Tamaño típico |
|---|---|
| PNG | ~812 KB |
| WebP | ~58 KB |
| SVG | ~36 KB |
> Los tamaños varían según la complejidad del contenido (los QR con más datos tienen más módulos).
---
## Recomendación por Caso de Uso
| Escenario | Formato recomendado |
|---|---|
| Mostrar en sitio/app | **WebP** |
| Enviar por e-mail | **PNG** |
| Impresión gráfica / diseño | **SVG** |
| Guardar en la nube | **WebP** |
| Máxima compatibilidad | **PNG** |
| Sin preocupación por tamaño | **PNG** |

View File

@ -0,0 +1,129 @@
---
title: "PNG, WebP ou SVG? Escolhendo o Formato Certo para seu QR Code"
description: "Comparativo técnico entre PNG, WebP e SVG para QR codes via API. Saiba qual formato usar dependendo do seu caso de uso: e-mail, impressão, web ou apps."
keywords: "qr code png, qr code webp, qr code svg, formato imagem qr code, api qr code formato"
author: "QRRapido"
date: 2026-03-08
lastmod: 2026-03-08
image: ""
---
# PNG, WebP ou SVG? Escolhendo o Formato Certo para seu QR Code
A API QRRapido suporta três formatos de saída: `png`, `webp` e `svg`. Cada um tem características distintas que impactam tamanho de arquivo, qualidade e compatibilidade. Use o parâmetro `outputFormat` na requisição para escolher.
```json
{
"content": "https://seusite.com",
"type": "url",
"outputFormat": "webp"
}
```
---
## Resumo Rápido
| Formato | Tamanho | Escala sem perda | Compatibilidade | Ideal para |
|---|---|---|---|---|
| **PNG** | Médio | Não (raster) | Universal | Impressão, e-mail, legado |
| **WebP** | Pequeno (~30% menor) | Não (raster) | Browsers modernos | Web, apps, APIs |
| **SVG** | Variável (geralmente menor) | **Sim** | Browsers, Adobe, Figma | Logos, banners, impressão vetorial |
---
## PNG — O Padrão Universal
PNG é um formato raster sem perda, ideal para QR codes porque preserva 100% dos pixels pretos e brancos sem introduzir artefatos.
**Quando usar:**
- Envio por e-mail (clientes de e-mail antigos não suportam WebP)
- Integração com sistemas legados
- Quando a compatibilidade máxima é prioridade
**Por que não JPEG?**
> JPEG usa compressão com perda que introduz artefatos de borrão nas bordas dos módulos do QR code. Isso pode tornar o código ilegível, especialmente em tamanhos menores. **Nunca use JPEG para QR codes.**
```json
{ "outputFormat": "png" }
```
---
## WebP — Recomendado para Web e Apps
WebP é o formato ideal para a maioria das integrações modernas. A qualidade visual é indistinguível do PNG para QR codes, com **2535% menos tamanho de arquivo**.
**Vantagens:**
- Carregamento mais rápido em páginas web
- Menor uso de bandwidth em APIs com alto volume
- Suporte nativo em todos os browsers modernos (Chrome, Safari 14+, Firefox, Edge)
- Menor consumo de armazenamento em buckets (S3, GCS, etc.)
**Quando usar:**
- Exibição em `<img>` em sites e aplicações web
- Apps iOS/Android (ambos suportam WebP)
- Quando você armazena os QR codes gerados
```json
{ "outputFormat": "webp" }
```
Para exibir no HTML:
```html
<picture>
<source srcset="data:image/webp;base64,{{ qrCodeBase64 }}" type="image/webp">
<img src="data:image/png;base64,{{ fallbackBase64 }}" alt="QR Code">
</picture>
```
---
## SVG — Para Escalar sem Perda
SVG é um formato vetorial: o QR code é descrito matematicamente, não como pixels. Isso significa que pode ser redimensionado para qualquer tamanho — de um ícone de 16px a um banner de 3 metros — sem perda de qualidade.
**Quando usar:**
- Material gráfico (banners, cartazes, embalagens, camisas)
- Integração com ferramentas de design (Figma, Illustrator, Canva)
- Impressão de alta qualidade sem depender de resolução
**Atenção:**
- Não use SVG para envio por e-mail (a maioria dos clientes bloqueia SVG por segurança)
- Alguns leitores de QR antigos podem ter dificuldade com SVG renderizado diretamente via `<img>`
```json
{ "outputFormat": "svg" }
```
Para exibir SVG inline, o `qrCodeBase64` deve ser decodificado primeiro:
```js
const svgString = atob(qrCodeBase64);
document.getElementById('qr').innerHTML = svgString;
```
---
## Comparativo de Tamanho (exemplo real — 400px, URL simples)
| Formato | Tamanho típico |
|---|---|
| PNG | ~812 KB |
| WebP | ~58 KB |
| SVG | ~36 KB |
> Os tamanhos variam conforme a complexidade do conteúdo (QR codes com mais dados têm mais módulos).
---
## Recomendação por Caso de Uso
| Cenário | Formato recomendado |
|---|---|
| Exibir em site/app | **WebP** |
| Enviar por e-mail | **PNG** |
| Impressão gráfica / design | **SVG** |
| Armazenar em cloud | **WebP** |
| Máxima compatibilidade | **PNG** |
| Sem preocupação com tamanho | **PNG** |

View File

@ -0,0 +1,186 @@
---
title: "How to Generate QR Codes via API: All Supported Types"
description: "Complete guide to generating QR codes via REST API using each available type: URL, Pix, Wi-Fi, vCard, WhatsApp, SMS, email and free text."
keywords: "api qr code, generate qr code api, qr code rest api, qrrapido api, qr code types"
author: "QRRapido"
date: 2026-03-08
lastmod: 2026-03-08
image: ""
---
# How to Generate QR Codes via API: All Supported Types
The QRRapido API supports 8 QR code types. All use the same endpoint — the only difference is the `type` field and the specific fields that make up the `content`.
## Endpoint
```
POST /api/v1/QRManager/generate
X-API-Key: your_key_here
Content-Type: application/json
```
## Base Request Structure
```json
{
"content": "...",
"type": "url",
"size": 400,
"primaryColor": "#000000",
"backgroundColor": "#FFFFFF",
"outputFormat": "png"
}
```
| Field | Type | Default | Description |
|---|---|---|---|
| `content` | string | required | QR code content |
| `type` | string | `"url"` | QR code type (see below) |
| `size` | int | `300` | Size in pixels (1002000) |
| `primaryColor` | string | `"#000000"` | Module color (hex) |
| `backgroundColor` | string | `"#FFFFFF"` | Background color (hex) |
| `outputFormat` | string | `"png"` | Format: `png`, `webp`, `svg` |
---
## Type `url` — Link / URL
The simplest: any valid URL.
```json
{
"type": "url",
"content": "https://yoursite.com/product/123"
}
```
> Use `https://` whenever possible. QR codes with HTTP may be blocked by some modern readers.
---
## Type `pix` — Pix Payment
The `content` must be the **Pix key** of the recipient. The generation produces a static Pix QR code (without connection to the Central Bank — see the dedicated Pix tutorial).
```json
{
"type": "pix",
"content": "contact@yourcompany.com"
}
```
Accepted keys: CPF/CNPJ (digits only), email, phone in `+5511999999999` format, or random key (UUID).
---
## Type `wifi` — Wi-Fi Network
The `content` uses the standard `WIFI:` format:
```json
{
"type": "wifi",
"content": "WIFI:S:NetworkName;T:WPA;P:NetworkPassword;;"
}
```
| Parameter | Meaning | Values |
|---|---|---|
| `S:` | SSID (network name) | text |
| `T:` | Security type | `WPA`, `WEP`, `nopass` |
| `P:` | Password | text |
| `H:` | Hidden network (optional) | `true` / `false` |
---
## Type `vcard` — Business Card
The `content` must be a complete vCard v3:
```json
{
"type": "vcard",
"content": "BEGIN:VCARD\nVERSION:3.0\nFN:John Smith\nORG:Company Inc\nTEL:+15559876543\nEMAIL:john@company.com\nURL:https://company.com\nEND:VCARD"
}
```
---
## Type `whatsapp` — WhatsApp Link
```json
{
"type": "whatsapp",
"content": "https://wa.me/15559876543?text=Hi%2C%20I%27d%20like%20to%20know%20more"
}
```
The number must include country code and area code, without spaces or symbols. The `text` parameter is optional.
---
## Type `email` — Email with subject and body
```json
{
"type": "email",
"content": "mailto:contact@company.com?subject=Subject&body=Initial%20message"
}
```
---
## Type `sms` — Pre-filled SMS
```json
{
"type": "sms",
"content": "sms:+15559876543?body=Hi%2C%20I%20want%20more%20information"
}
```
---
## Type `texto` — Free Text
Any string up to 2048 characters:
```json
{
"type": "texto",
"content": "Table 12 — Priority service available at the main counter."
}
```
---
## Success Response
```json
{
"success": true,
"qrCodeBase64": "iVBORw0KGgo...",
"qrId": "abc123",
"generationTimeMs": 180,
"remainingCredits": 42,
"fromCache": false,
"format": "png",
"mimeType": "image/png"
}
```
To display the image directly in HTML:
```html
<img src="data:image/png;base64,{{ qrCodeBase64 }}" alt="QR Code" />
```
---
## General Tips
- **Colors**: maintain high contrast between `primaryColor` and `backgroundColor`. Avoid yellow on white or light gray on white.
- **Minimum printed size**: use `size` ≥ 400 for printing. For digital use (screens), 300 is sufficient.
- **Cache**: identical requests return `fromCache: true` without consuming additional credit.
- **Rate limit**: the `X-RateLimit-Remaining` and `X-Quota-Remaining` headers in the response indicate your current balance.

View File

@ -0,0 +1,186 @@
---
title: "Cómo Generar Códigos QR por la API: Todos los Tipos"
description: "Guía completa para generar códigos QR vía API REST usando cada tipo disponible: URL, Pix, Wi-Fi, vCard, WhatsApp, SMS, e-mail y texto libre."
keywords: "api qr code, generar qr code api, qr code rest api, qrrapido api, tipos qr code paraguay"
author: "QRRapido"
date: 2026-03-08
lastmod: 2026-03-08
image: ""
---
# Cómo Generar Códigos QR por la API: Todos los Tipos
La API QRRapido soporta 8 tipos de código QR. Todos usan el mismo endpoint — lo que cambia es el campo `type` ha los campos específicos que forman el `content`.
## Endpoint
```
POST /api/v1/QRManager/generate
X-API-Key: tu_clave_aqui
Content-Type: application/json
```
## Estructura Base de la Solicitud
```json
{
"content": "...",
"type": "url",
"size": 400,
"primaryColor": "#000000",
"backgroundColor": "#FFFFFF",
"outputFormat": "png"
}
```
| Campo | Tipo | Por defecto | Descripción |
|---|---|---|---|
| `content` | string | obligatorio | Contenido del QR |
| `type` | string | `"url"` | Tipo de QR (ver abajo) |
| `size` | int | `300` | Tamaño en píxeles (1002000) |
| `primaryColor` | string | `"#000000"` | Color de los módulos (hex) |
| `backgroundColor` | string | `"#FFFFFF"` | Color de fondo (hex) |
| `outputFormat` | string | `"png"` | Formato: `png`, `webp`, `svg` |
---
## Tipo `url` — Link / URL
El más simple: cualquier URL válida.
```json
{
"type": "url",
"content": "https://tuempresa.com.py/producto/123"
}
```
> Usá `https://` siempre que sea posible. Los QR con HTTP pueden ser bloqueados por algunos lectores modernos.
---
## Tipo `pix` — Pago Pix
El `content` debe ser la **clave Pix** del receptor. La generación produce un QR de Pix estático (sin conexión con el Banco Central — mirá el tutorial dedicado sobre Pix).
```json
{
"type": "pix",
"content": "contacto@tuempresa.com.br"
}
```
Claves aceptadas: CPF/CNPJ (solo dígitos), e-mail, teléfono en formato `+5511999999999`, o clave aleatoria (UUID).
---
## Tipo `wifi` — Red Wi-Fi
El `content` usa el formato estándar `WIFI:`:
```json
{
"type": "wifi",
"content": "WIFI:S:NombreDeRed;T:WPA;P:ContraseñaDeRed;;"
}
```
| Parámetro | Significado | Valores |
|---|---|---|
| `S:` | SSID (nombre de la red) | texto |
| `T:` | Tipo de seguridad | `WPA`, `WEP`, `nopass` |
| `P:` | Contraseña | texto |
| `H:` | Red oculta (opcional) | `true` / `false` |
---
## Tipo `vcard` — Tarjeta de Visita
El `content` debe ser un vCard v3 completo:
```json
{
"type": "vcard",
"content": "BEGIN:VCARD\nVERSION:3.0\nFN:Juan Pérez\nORG:Empresa SRL\nTEL:+595981999999\nEMAIL:juan@empresa.com.py\nURL:https://empresa.com.py\nEND:VCARD"
}
```
---
## Tipo `whatsapp` — Link para WhatsApp
```json
{
"type": "whatsapp",
"content": "https://wa.me/595981999999?text=Mba'éichapa%2C%20quiero%20más%20información"
}
```
El número debe incluir código de país ha área, sin espacios ni símbolos. El parámetro `text` es opcional.
---
## Tipo `email` — E-mail con asunto ha cuerpo
```json
{
"type": "email",
"content": "mailto:contacto@empresa.com.py?subject=Consulta&body=Mba'éichapa%2C%20quisiera%20saber%20más"
}
```
---
## Tipo `sms` — SMS pre-cargado
```json
{
"type": "sms",
"content": "sms:+595981999999?body=Mba'éichapa%2C%20quiero%20más%20información"
}
```
---
## Tipo `texto` — Texto Libre
Cualquier string de hasta 2048 caracteres:
```json
{
"type": "texto",
"content": "Mesa 5 — Atención preferencial disponible en el mostrador central. Porã roipytyvõ!"
}
```
---
## Respuesta Exitosa
```json
{
"success": true,
"qrCodeBase64": "iVBORw0KGgo...",
"qrId": "abc123",
"generationTimeMs": 180,
"remainingCredits": 42,
"fromCache": false,
"format": "png",
"mimeType": "image/png"
}
```
Para mostrar la imagen directamente en HTML:
```html
<img src="data:image/png;base64,{{ qrCodeBase64 }}" alt="Código QR" />
```
---
## Consejos Generales
- **Colores**: mantené alto contraste entre `primaryColor` ha `backgroundColor`. Evitá amarillo sobre blanco o gris claro sobre blanco.
- **Tamaño mínimo impreso**: usá `size` ≥ 400 para impresiones. Para uso digital, 300 es suficiente.
- **Caché**: solicitudes idénticas devuelven `fromCache: true` sin consumir crédito adicional.
- **Rate limit**: los headers `X-RateLimit-Remaining` ha `X-Quota-Remaining` en la respuesta indican tu saldo actual.

View File

@ -0,0 +1,186 @@
---
title: "Cómo Generar Códigos QR por la API: Todos los Tipos"
description: "Guía completa para generar códigos QR vía API REST usando cada tipo disponible: URL, Pix, Wi-Fi, vCard, WhatsApp, SMS, e-mail y texto libre."
keywords: "api qr code, generar qr code api, qr code rest api, qrrapido api, tipos qr code"
author: "QRRapido"
date: 2026-03-08
lastmod: 2026-03-08
image: ""
---
# Cómo Generar Códigos QR por la API: Todos los Tipos
La API QRRapido soporta 8 tipos de código QR. Todos usan el mismo endpoint — lo que cambia es el campo `type` y los campos específicos que forman el `content`.
## Endpoint
```
POST /api/v1/QRManager/generate
X-API-Key: tu_clave_aqui
Content-Type: application/json
```
## Estructura Base de la Solicitud
```json
{
"content": "...",
"type": "url",
"size": 400,
"primaryColor": "#000000",
"backgroundColor": "#FFFFFF",
"outputFormat": "png"
}
```
| Campo | Tipo | Por defecto | Descripción |
|---|---|---|---|
| `content` | string | obligatorio | Contenido del QR |
| `type` | string | `"url"` | Tipo de QR (ver abajo) |
| `size` | int | `300` | Tamaño en píxeles (1002000) |
| `primaryColor` | string | `"#000000"` | Color de los módulos (hex) |
| `backgroundColor` | string | `"#FFFFFF"` | Color de fondo (hex) |
| `outputFormat` | string | `"png"` | Formato: `png`, `webp`, `svg` |
---
## Tipo `url` — Link / URL
El más simple: cualquier URL válida.
```json
{
"type": "url",
"content": "https://tuempresa.com/producto/123"
}
```
> Usa `https://` siempre que sea posible. Los QR con HTTP pueden ser bloqueados por algunos lectores modernos.
---
## Tipo `pix` — Pago Pix
El `content` debe ser la **clave Pix** del receptor. La generación produce un QR de Pix estático (sin conexión con el Banco Central — consulta el tutorial dedicado sobre Pix).
```json
{
"type": "pix",
"content": "contacto@tuempresa.com.br"
}
```
Claves aceptadas: CPF/CNPJ (solo dígitos), e-mail, teléfono en formato `+5511999999999`, o clave aleatoria (UUID).
---
## Tipo `wifi` — Red Wi-Fi
El `content` usa el formato estándar `WIFI:`:
```json
{
"type": "wifi",
"content": "WIFI:S:NombreDeRed;T:WPA;P:ContraseñaDeRed;;"
}
```
| Parámetro | Significado | Valores |
|---|---|---|
| `S:` | SSID (nombre de la red) | texto |
| `T:` | Tipo de seguridad | `WPA`, `WEP`, `nopass` |
| `P:` | Contraseña | texto |
| `H:` | Red oculta (opcional) | `true` / `false` |
---
## Tipo `vcard` — Tarjeta de Visita
El `content` debe ser un vCard v3 completo:
```json
{
"type": "vcard",
"content": "BEGIN:VCARD\nVERSION:3.0\nFN:Juan Pérez\nORG:Empresa SRL\nTEL:+5491199999999\nEMAIL:juan@empresa.com\nURL:https://empresa.com\nEND:VCARD"
}
```
---
## Tipo `whatsapp` — Link para WhatsApp
```json
{
"type": "whatsapp",
"content": "https://wa.me/5491199999999?text=¡Hola!%20Quisiera%20más%20información"
}
```
El número debe incluir código de país y área, sin espacios ni símbolos. El parámetro `text` es opcional.
---
## Tipo `email` — E-mail con asunto y cuerpo
```json
{
"type": "email",
"content": "mailto:contacto@empresa.com?subject=Consulta&body=¡Hola!%20Quisiera%20saber%20más"
}
```
---
## Tipo `sms` — SMS pre-cargado
```json
{
"type": "sms",
"content": "sms:+5491199999999?body=¡Hola!%20Quiero%20más%20información"
}
```
---
## Tipo `texto` — Texto Libre
Cualquier string de hasta 2048 caracteres:
```json
{
"type": "texto",
"content": "Mesa 5 — Atención preferencial disponible en el mostrador central."
}
```
---
## Respuesta Exitosa
```json
{
"success": true,
"qrCodeBase64": "iVBORw0KGgo...",
"qrId": "abc123",
"generationTimeMs": 180,
"remainingCredits": 42,
"fromCache": false,
"format": "png",
"mimeType": "image/png"
}
```
Para mostrar la imagen directamente en HTML:
```html
<img src="data:image/png;base64,{{ qrCodeBase64 }}" alt="Código QR" />
```
---
## Consejos Generales
- **Colores**: mantén alto contraste entre `primaryColor` y `backgroundColor`. Evita amarillo sobre blanco o gris claro sobre blanco.
- **Tamaño mínimo impreso**: usa `size` ≥ 400 para impresiones. Para uso digital, 300 es suficiente.
- **Caché**: solicitudes idénticas devuelven `fromCache: true` sin consumir crédito adicional.
- **Rate limit**: los headers `X-RateLimit-Remaining` y `X-Quota-Remaining` en la respuesta indican tu saldo actual.

View File

@ -0,0 +1,186 @@
---
title: "Como Gerar QR Codes pela API: Todos os Tipos Suportados"
description: "Guia completo para gerar QR codes via API REST usando cada tipo disponível: URL, Pix, Wi-Fi, vCard, WhatsApp, SMS, e-mail e texto livre."
keywords: "api qr code, gerar qr code api, qr code rest api, qrrapido api, tipos qr code"
author: "QRRapido"
date: 2026-03-08
lastmod: 2026-03-08
image: ""
---
# Como Gerar QR Codes pela API: Todos os Tipos Suportados
A API QRRapido suporta 8 tipos de QR code. Todos usam o mesmo endpoint — o que muda é o campo `type` e os campos específicos que compõem o `content`.
## Endpoint
```
POST /api/v1/QRManager/generate
X-API-Key: sua_chave_aqui
Content-Type: application/json
```
## Estrutura Base da Requisição
```json
{
"content": "...",
"type": "url",
"size": 400,
"primaryColor": "#000000",
"backgroundColor": "#FFFFFF",
"outputFormat": "png"
}
```
| Campo | Tipo | Padrão | Descrição |
|---|---|---|---|
| `content` | string | obrigatório | Conteúdo do QR code |
| `type` | string | `"url"` | Tipo do QR code (ver abaixo) |
| `size` | int | `300` | Tamanho em pixels (1002000) |
| `primaryColor` | string | `"#000000"` | Cor dos módulos (hex) |
| `backgroundColor` | string | `"#FFFFFF"` | Cor de fundo (hex) |
| `outputFormat` | string | `"png"` | Formato: `png`, `webp`, `svg` |
---
## Tipo `url` — Link / URL
O mais simples: qualquer URL válida.
```json
{
"type": "url",
"content": "https://seusite.com.br/produto/123"
}
```
> Use `https://` sempre que possível. QR codes com HTTP podem ser bloqueados por alguns leitores modernos.
---
## Tipo `pix` — Pagamento Pix
O `content` deve ser a **chave Pix** do recebedor. A geração produz um QR de Pix estático (sem conexão com o Banco Central — veja o tutorial dedicado sobre Pix).
```json
{
"type": "pix",
"content": "contato@suaempresa.com.br"
}
```
Chaves aceitas: CPF/CNPJ (apenas dígitos), e-mail, telefone no formato `+5511999999999`, ou chave aleatória (UUID).
---
## Tipo `wifi` — Rede Wi-Fi
O `content` usa o formato padrão `WIFI:`:
```json
{
"type": "wifi",
"content": "WIFI:S:NomeDaRede;T:WPA;P:SenhaDaRede;;"
}
```
| Parâmetro | Significado | Valores |
|---|---|---|
| `S:` | SSID (nome da rede) | texto |
| `T:` | Tipo de segurança | `WPA`, `WEP`, `nopass` |
| `P:` | Senha | texto |
| `H:` | Rede oculta (opcional) | `true` / `false` |
---
## Tipo `vcard` — Cartão de Visita
O `content` deve ser um vCard v3 completo:
```json
{
"type": "vcard",
"content": "BEGIN:VCARD\nVERSION:3.0\nFN:João Silva\nORG:Empresa Ltda\nTEL:+5511999999999\nEMAIL:joao@empresa.com\nURL:https://empresa.com\nEND:VCARD"
}
```
---
## Tipo `whatsapp` — Link para WhatsApp
```json
{
"type": "whatsapp",
"content": "https://wa.me/5511999999999?text=Olá%2C%20gostaria%20de%20saber%20mais"
}
```
O número deve incluir código do país e DDD, sem espaços ou símbolos. O parâmetro `text` é opcional.
---
## Tipo `email` — E-mail com assunto e corpo
```json
{
"type": "email",
"content": "mailto:contato@empresa.com?subject=Assunto&body=Mensagem%20inicial"
}
```
---
## Tipo `sms` — SMS pré-preenchido
```json
{
"type": "sms",
"content": "sms:+5511999999999?body=Olá%2C%20quero%20mais%20informações"
}
```
---
## Tipo `texto` — Texto Livre
Qualquer string de até 2048 caracteres:
```json
{
"type": "texto",
"content": "Mesa 12 — Atendimento preferencial disponível no balcão central."
}
```
---
## Resposta de Sucesso
```json
{
"success": true,
"qrCodeBase64": "iVBORw0KGgo...",
"qrId": "abc123",
"generationTimeMs": 180,
"remainingCredits": 42,
"fromCache": false,
"format": "png",
"mimeType": "image/png"
}
```
Para exibir a imagem diretamente no HTML:
```html
<img src="data:image/png;base64,{{ qrCodeBase64 }}" alt="QR Code" />
```
---
## Dicas Gerais
- **Cores**: mantenha alto contraste entre `primaryColor` e `backgroundColor`. Evite amarelo sobre branco ou cinza claro sobre branco.
- **Tamanho mínimo impresso**: use `size` ≥ 400 para impressões. Para uso digital (telas), 300 é suficiente.
- **Cache**: requisições idênticas retornam `fromCache: true` sem consumir crédito adicional.
- **Rate limit**: os headers `X-RateLimit-Remaining` e `X-Quota-Remaining` na resposta indicam seu saldo atual.

View File

@ -0,0 +1,142 @@
---
title: "Static Pix QR Code: How It Works and Developer Responsibilities"
description: "Understand what a static Pix QR Code is, how it is generated by the API, its limitations, and why payment verification is your application's responsibility — not QRRapido's."
keywords: "pix qr code, static pix, pix qr code api, pix qr code integration, pix without central bank"
author: "QRRapido"
date: 2026-03-08
lastmod: 2026-03-08
image: ""
---
# Static Pix QR Code: How It Works and Developer Responsibilities
## What is a Static Pix QR Code?
Pix has two types of QR codes: **static** and **dynamic**.
| | Static Pix | Dynamic Pix |
|---|---|---|
| Amount | Variable (payer decides) | Fixed (set by recipient) |
| Generation | Any system | PSP/Central Bank intermediary |
| Central Bank registration | **No** | Yes |
| Payment notification | **No** | Yes (webhook) |
| Typical use | Donations, simple charges | E-commerce, tracked billing |
The QRRapido API generates **exclusively static Pix QR codes**. This is deliberate and sufficient for most use cases.
---
## How the API Generates the Pix QR Code
When you send a request with `type: "pix"`, the API assembles a string in the **EMV® QR Code** standard (the international standard adopted by Brazil's Central Bank) and generates the image:
```json
{
"type": "pix",
"content": "contact@yourcompany.com"
}
```
The `content` field must contain **only the Pix key** of the recipient. The API handles assembling the EMV payload correctly.
**Accepted keys:**
- Email: `contact@company.com`
- CPF/CNPJ: digits only — `12345678901` or `12345678000195`
- Phone: `+5511999999999`
- Random key (EVP): `123e4567-e89b-12d3-a456-426614174000`
---
## What the API Does NOT Do (and Why This Matters)
> **QRRapido has no integration with the Central Bank, any bank, or any PSP (Payment Service Provider).**
This means:
1. **The API does not know if the payment was made.** It only generates the QR code image. What happens after — whether the customer scanned it, whether the Pix was sent, whether it landed in the right account — is outside the scope of the API.
2. **There is no payment confirmation webhook.** The API does not send notifications when a Pix is received.
3. **The QR code does not expire automatically.** A static Pix QR code is valid indefinitely (or until the Pix key is removed by the recipient at the bank).
4. **There is no transaction traceability.** Two requests with the same key generate the same QR code (with cache). It is not possible to associate a scan with a specific transaction via the API.
---
## Your Application's Responsibility
If you use the API to generate Pix QR codes in a payment flow, **it is your application's responsibility to verify receipt**. The correct ways to do this are:
### Option 1 — Recipient's Bank Pix API
Connect directly to the Pix API of the bank where the key is registered. Most banks offer:
- Query of received charges
- Real-time notification webhook (when the bank supports it)
### Option 2 — PSP / Payment Gateway
Use an intermediary such as Mercado Pago, PagSeguro, Efí Bank, Asaas, etc. They offer dynamic Pix with full control: fixed amount, expiration, webhooks and unique identification per charge.
### Option 3 — Manual Verification
For low volumes or informal contexts (donations, in-person sales), the person responsible for receiving verifies the bank statement manually.
---
## Recommended Flow (with confirmation)
```
Your App QRRapido API Recipient's Bank
| | |
|-- POST /generate (pix) -->| |
|<-- qrCodeBase64 ----------| |
| | |
|-- Shows QR to customer | |
| | |
|-- Query payment ---------------------------------->|
|<-- Received status or not --------------------------|
| | |
|-- Releases product/service| |
```
---
## Complete Request Example
```bash
curl -X POST https://qrrapido.site/api/v1/QRManager/generate \
-H "X-API-Key: your_key_here" \
-H "Content-Type: application/json" \
-d '{
"content": "contact@yourcompany.com",
"type": "pix",
"size": 400,
"outputFormat": "webp"
}'
```
---
## Appropriate Use Cases for Static Pix via API
- Donation QR code on an institutional website
- Digital menu with key for in-person payment
- Batch QR generation for printed materials (flyers, cards)
- Applications where the seller and buyer interact in person and the seller confirms receipt in the bank app
## Use Cases That Require Dynamic Pix (not supported by this API)
- E-commerce with automatic order confirmation
- Fixed-amount billing with expiration
- Invoice issuance linked to payment
- Automated financial reconciliation
---
## Summary
| What the API does | What the API does NOT do |
|---|---|
| Generates the Pix QR code image | Verifies if the payment was made |
| Formats the EMV payload correctly | Sends confirmation webhook |
| Delivers PNG, WebP or SVG | Communicates with the Central Bank or banks |
| Works with any valid Pix key | Guarantees the key belongs to who claims it does |
Correct QR code generation is the API's responsibility. **Payment confirmation is your application's responsibility.**

View File

@ -0,0 +1,142 @@
---
title: "Código QR Pix Estático: Cómo Funciona y Responsabilidades del Desarrollador"
description: "Entendé qué es el QR Pix estático, cómo lo genera la API, sus limitaciones ha por qué la verificación del pago es responsabilidad de tu aplicación — no de QRRapido."
keywords: "qr code pix, pix estatico, qr code pix api, integración pix qr code paraguay, pix sin bacen"
author: "QRRapido"
date: 2026-03-08
lastmod: 2026-03-08
image: ""
---
# Código QR Pix Estático: Cómo Funciona y Responsabilidades del Desarrollador
## ¿Qué es un Código QR Pix Estático?
El Pix tiene dos tipos de QR: **estático** ha **dinámico**.
| | Pix Estático | Pix Dinámico |
|---|---|---|
| Valor | Variable (lo decide el pagador) | Fijo (definido por el receptor) |
| Generación | Cualquier sistema | Intermediario PSP/BACEN |
| Registro en BACEN | **No** | Sí |
| Notificación de pago | **No** | Sí (webhook) |
| Uso típico | Donaciones, cobros simples | E-commerce, cobro con control |
La API QRRapido genera **exclusivamente QR Pix estático**. Eso es intencional ha suficiente para la mayoría de los casos de uso.
---
## Cómo la API Genera el QR Pix
Al enviar una solicitud con `type: "pix"`, la API arma un string en el estándar **EMV® QR Code** (estándar internacional adoptado por el Banco Central de Brasil) ha genera la imagen:
```json
{
"type": "pix",
"content": "contacto@tuempresa.com.br"
}
```
El campo `content` debe contener **solo la clave Pix** del receptor. La API se encarga de armar el payload EMV correctamente.
**Claves aceptadas:**
- E-mail: `contacto@empresa.com.br`
- CPF/CNPJ: solo dígitos — `12345678901` o `12345678000195`
- Teléfono: `+5511999999999`
- Clave aleatoria (EVP): `123e4567-e89b-12d3-a456-426614174000`
---
## Lo que la API NO hace (ha por qué eso importa)
> **QRRapido no tiene integración con el Banco Central, con ningún banco ni PSP (Proveedor de Servicio de Pago).**
Eso significa:
1. **La API no sabe si el pago fue realizado.** Solo genera la imagen del QR. Lo que pasa después — si el cliente escaneó, si el Pix fue enviado, si llegó a la cuenta correcta — está fuera del alcance de la API.
2. **No hay webhook de confirmación de pago.** La API no envía notificaciones cuando se recibe un Pix.
3. **El QR no expira automáticamente.** Un QR Pix estático es válido indefinidamente (o hasta que la clave Pix sea eliminada por el receptor en su banco).
4. **No hay rastreabilidad de transacción.** Dos solicitudes con la misma clave generan el mismo QR (con caché). No es posible asociar un escaneo a una transacción específica vía API.
---
## Responsabilidad de Tu Aplicación
Si usás la API para generar QR Pix en un flujo de pago, **es responsabilidad de tu aplicación verificar el cobro**. Las formas correctas de hacerlo son:
### Opción 1 — API Pix del Banco del Receptor
Conectate directamente a la API Pix del banco donde está registrada la clave. La mayoría de los bancos ofrece:
- Consulta de cobros recibidos
- Webhook de notificación en tiempo real (cuando el banco lo soporta)
### Opción 2 — PSP / Gateway de Pago
Usá un intermediario como Mercado Pago, PagSeguro, Efí Bank, Asaas, etc. Ofrecen Pix dinámico con control completo: valor fijo, expiración, webhooks e identificación única por cobro.
### Opción 3 — Verificación Manual
Para volúmenes bajos o contextos informales (donaciones, ventas presenciales), el responsable del cobro verifica el extracto bancario manualmente.
---
## Flujo Recomendado (con confirmación)
```
Tu App API QRRapido Banco del Receptor
| | |
|-- POST /generate (pix) -->| |
|<-- qrCodeBase64 ----------| |
| | |
|-- Muestra QR al cliente | |
| | |
|-- Consulta pago ---------------------------------->|
|<-- Estado recibido o no ----------------------------|
| | |
|-- Libera producto/servicio| |
```
---
## Ejemplo de Solicitud Completa
```bash
curl -X POST https://qrrapido.site/api/v1/QRManager/generate \
-H "X-API-Key: tu_clave_aqui" \
-H "Content-Type: application/json" \
-d '{
"content": "contacto@tuempresa.com.br",
"type": "pix",
"size": 400,
"outputFormat": "webp"
}'
```
---
## Casos de Uso Adecuados para Pix Estático vía API
- QR de donación en sitio institucional
- Menú digital con clave para pago presencial
- Generación de QR en lote para materiales impresos (flyers, tarjetas)
- Aplicaciones donde el vendedor ha comprador interactúan presencialmente ha el vendedor confirma el cobro en la app del banco
## Casos de Uso que Requieren Pix Dinámico (no cubiertos por esta API)
- E-commerce con confirmación automática del pedido
- Cobro con valor fijo ha expiración
- Emisión de factura vinculada al pago
- Conciliación financiera automatizada
---
## Resumen
| Lo que la API hace | Lo que la API NO hace |
|---|---|
| Genera la imagen del QR Pix | Verifica si el pago fue hecho |
| Formatea el payload EMV correctamente | Envía webhook de confirmación |
| Entrega PNG, WebP o SVG | Se comunica con el BACEN o bancos |
| Funciona con cualquier clave Pix válida | Garantiza que la clave pertenece a quien dice |
La generación correcta del QR es responsabilidad de la API. La **confirmación del pago es responsabilidad de tu aplicación** — ha eso, ñande lo tenemos claro ko'aga.

View File

@ -0,0 +1,142 @@
---
title: "Código QR Pix Estático: Cómo Funciona y Responsabilidades del Desarrollador"
description: "Entiende qué es el QR Pix estático, cómo lo genera la API, sus limitaciones y por qué la verificación del pago es responsabilidad de tu aplicación — no de QRRapido."
keywords: "qr code pix, pix estatico, qr code pix api, integración pix qr code, pix sin bacen"
author: "QRRapido"
date: 2026-03-08
lastmod: 2026-03-08
image: ""
---
# Código QR Pix Estático: Cómo Funciona y Responsabilidades del Desarrollador
## ¿Qué es un Código QR Pix Estático?
El Pix tiene dos tipos de QR: **estático** y **dinámico**.
| | Pix Estático | Pix Dinámico |
|---|---|---|
| Valor | Variable (lo decide el pagador) | Fijo (definido por el receptor) |
| Generación | Cualquier sistema | Intermediario PSP/BACEN |
| Registro en BACEN | **No** | Sí |
| Notificación de pago | **No** | Sí (webhook) |
| Uso típico | Donaciones, cobros simples | E-commerce, cobro con control |
La API QRRapido genera **exclusivamente QR Pix estático**. Eso es intencional y suficiente para la mayoría de los casos de uso.
---
## Cómo la API Genera el QR Pix
Al enviar una solicitud con `type: "pix"`, la API arma un string en el estándar **EMV® QR Code** (estándar internacional adoptado por el Banco Central de Brasil) y genera la imagen:
```json
{
"type": "pix",
"content": "contacto@tuempresa.com.br"
}
```
El campo `content` debe contener **solo la clave Pix** del receptor. La API se encarga de armar el payload EMV correctamente.
**Claves aceptadas:**
- E-mail: `contacto@empresa.com.br`
- CPF/CNPJ: solo dígitos — `12345678901` o `12345678000195`
- Teléfono: `+5511999999999`
- Clave aleatoria (EVP): `123e4567-e89b-12d3-a456-426614174000`
---
## Lo que la API NO hace (y por qué eso importa)
> **QRRapido no tiene integración con el Banco Central, con ningún banco ni PSP (Proveedor de Servicio de Pago).**
Eso significa:
1. **La API no sabe si el pago fue realizado.** Solo genera la imagen del QR. Lo que pasa después — si el cliente escaneó, si el Pix fue enviado, si llegó a la cuenta correcta — está fuera del alcance de la API.
2. **No hay webhook de confirmación de pago.** La API no envía notificaciones cuando se recibe un Pix.
3. **El QR no expira automáticamente.** Un QR Pix estático es válido indefinidamente (o hasta que la clave Pix sea eliminada por el receptor en su banco).
4. **No hay rastreabilidad de transacción.** Dos solicitudes con la misma clave generan el mismo QR (con caché). No es posible asociar un escaneo a una transacción específica vía API.
---
## Responsabilidad de Tu Aplicación
Si usas la API para generar QR Pix en un flujo de pago, **es responsabilidad de tu aplicación verificar el cobro**. Las formas correctas de hacerlo son:
### Opción 1 — API Pix del Banco del Receptor
Conéctate directamente a la API Pix del banco donde está registrada la clave. La mayoría de los bancos ofrece:
- Consulta de cobros recibidos
- Webhook de notificación en tiempo real (cuando el banco lo soporta)
### Opción 2 — PSP / Gateway de Pago
Usa un intermediario como Mercado Pago, PagSeguro, Efí Bank, Asaas, etc. Ofrecen Pix dinámico con control completo: valor fijo, expiración, webhooks e identificación única por cobro.
### Opción 3 — Verificación Manual
Para volúmenes bajos o contextos informales (donaciones, ventas presenciales), el responsable del cobro verifica el extracto bancario manualmente.
---
## Flujo Recomendado (con confirmación)
```
Tu App API QRRapido Banco del Receptor
| | |
|-- POST /generate (pix) -->| |
|<-- qrCodeBase64 ----------| |
| | |
|-- Muestra QR al cliente | |
| | |
|-- Consulta pago ---------------------------------->|
|<-- Estado recibido o no ----------------------------|
| | |
|-- Libera producto/servicio| |
```
---
## Ejemplo de Solicitud Completa
```bash
curl -X POST https://qrrapido.site/api/v1/QRManager/generate \
-H "X-API-Key: tu_clave_aqui" \
-H "Content-Type: application/json" \
-d '{
"content": "contacto@tuempresa.com.br",
"type": "pix",
"size": 400,
"outputFormat": "webp"
}'
```
---
## Casos de Uso Adecuados para Pix Estático vía API
- QR de donación en sitio institucional
- Menú digital con clave para pago presencial
- Generación de QR en lote para materiales impresos (flyers, tarjetas)
- Aplicaciones donde el vendedor y comprador interactúan presencialmente y el vendedor confirma el cobro en la app del banco
## Casos de Uso que Requieren Pix Dinámico (no cubiertos por esta API)
- E-commerce con confirmación automática del pedido
- Cobro con valor fijo y expiración
- Emisión de factura vinculada al pago
- Conciliación financiera automatizada
---
## Resumen
| Lo que la API hace | Lo que la API NO hace |
|---|---|
| Genera la imagen del QR Pix | Verifica si el pago fue hecho |
| Formatea el payload EMV correctamente | Envía webhook de confirmación |
| Entrega PNG, WebP o SVG | Se comunica con el BACEN o bancos |
| Funciona con cualquier clave Pix válida | Garantiza que la clave pertenece a quien dice |
La generación correcta del QR es responsabilidad de la API. La **confirmación del pago es responsabilidad de tu aplicación** — y eso queda claro desde el principio.

View File

@ -0,0 +1,142 @@
---
title: "QR Code Pix Estático: Como Funciona e Responsabilidades do Desenvolvedor"
description: "Entenda o que é o QR Code Pix estático, como ele é gerado pela API, suas limitações e por que a verificação do pagamento é responsabilidade da sua aplicação — não da QRRapido."
keywords: "qr code pix, pix estatico, qr code pix api, integração pix qr code, pix sem bacen"
author: "QRRapido"
date: 2026-03-08
lastmod: 2026-03-08
image: ""
---
# QR Code Pix Estático: Como Funciona e Responsabilidades do Desenvolvedor
## O que é um QR Code Pix Estático?
O Pix tem dois tipos de QR code: **estático** e **dinâmico**.
| | Pix Estático | Pix Dinâmico |
|---|---|---|
| Valor | Variável (pagador decide) | Fixo (definido pelo recebedor) |
| Geração | Qualquer sistema | Intermediário PSP/BACEN |
| Registro no BACEN | **Não** | Sim |
| Notificação de pagamento | **Não** | Sim (webhook) |
| Uso típico | Doações, cobranças simples | E-commerce, cobrança com controle |
A API QRRapido gera **exclusivamente QR code Pix estático**. Isso é deliberado e suficiente para a maioria dos casos de uso.
---
## Como a API Gera o QR Code Pix
Ao enviar uma requisição com `type: "pix"`, a API monta uma string no padrão **EMV® QR Code** (padrão internacional adotado pelo Banco Central do Brasil) e gera a imagem:
```json
{
"type": "pix",
"content": "contato@suaempresa.com.br"
}
```
O campo `content` deve conter **apenas a chave Pix** do recebedor. A API cuida de montar o payload EMV corretamente.
**Chaves aceitas:**
- E-mail: `contato@empresa.com.br`
- CPF/CNPJ: apenas dígitos — `12345678901` ou `12345678000195`
- Telefone: `+5511999999999`
- Chave aleatória (EVP): `123e4567-e89b-12d3-a456-426614174000`
---
## O que a API NÃO faz (e por quê isso importa)
> **A QRRapido não tem integração com o Banco Central, com nenhum banco ou PSP (Provedor de Serviço de Pagamento).**
Isso significa:
1. **A API não sabe se o pagamento foi realizado.** Ela apenas gera a imagem do QR code. O que acontece depois — se o cliente escaneou, se o Pix foi enviado, se caiu na conta certa — está fora do escopo da API.
2. **Não há webhook de confirmação de pagamento.** A API não envia notificações quando um Pix é recebido.
3. **O QR code não expira automaticamente.** Um QR code Pix estático é válido indefinidamente (ou até a chave Pix ser removida pelo recebedor no banco).
4. **Não há rastreabilidade de transação.** Duas requisições com a mesma chave geram o mesmo QR code (com cache). Não é possível associar um escaneamento a uma transação específica via API.
---
## Responsabilidade da Sua Aplicação
Se você usa a API para gerar QR codes Pix em um fluxo de pagamento, **é responsabilidade da sua aplicação verificar o recebimento**. As formas corretas de fazer isso são:
### Opção 1 — API Pix do Banco do Recebedor
Conecte-se diretamente à API Pix do banco onde está registrada a chave. A maioria dos bancos oferece:
- Consulta de cobranças recebidas
- Webhook de notificação em tempo real (quando o banco suporta)
### Opção 2 — PSP / Gateway de Pagamento
Use um intermediário como Mercado Pago, PagSeguro, Efí Bank, Asaas, etc. Eles oferecem Pix dinâmico com controle completo: valor fixo, expiração, webhooks e identificação única por cobrança.
### Opção 3 — Conferência Manual
Para volumes baixos ou contextos informais (doações, vendas presenciais), o responsável pelo recebimento verifica o extrato bancário manualmente.
---
## Fluxo Recomendado (com confirmação)
```
Sua App API QRRapido Banco do Recebedor
| | |
|-- POST /generate (pix) -->| |
|<-- qrCodeBase64 ----------| |
| | |
|-- Exibe QR para cliente | |
| | |
|-- Consulta pagamento ------------------------------>|
|<-- Status recebido ou não --------------------------|
| | |
|-- Libera produto/serviço | |
```
---
## Exemplo de Requisição Completa
```bash
curl -X POST https://qrrapido.site/api/v1/QRManager/generate \
-H "X-API-Key: sua_chave_aqui" \
-H "Content-Type: application/json" \
-d '{
"content": "contato@suaempresa.com.br",
"type": "pix",
"size": 400,
"outputFormat": "webp"
}'
```
---
## Casos de Uso Adequados para Pix Estático via API
- QR code de doação em site institucional
- Cardápio digital com chave para pagamento presencial
- Geração de QR em lote para materiais impressos (flyers, cartões)
- Aplicativos onde o vendedor e comprador interagem pessoalmente e o vendedor confirma o recebimento no app do banco
## Casos de Uso que Requerem Pix Dinâmico (não atendidos por esta API)
- E-commerce com confirmação automática do pedido
- Cobrança com valor fixo e expiração
- Emissão de nota fiscal vinculada ao pagamento
- Conciliação financeira automatizada
---
## Resumo
| O que a API faz | O que a API NÃO faz |
|---|---|
| Gera a imagem do QR code Pix | Verifica se o pagamento foi feito |
| Formata o payload EMV corretamente | Envia webhook de confirmação |
| Entrega PNG, WebP ou SVG | Se comunica com o BACEN ou bancos |
| Funciona com qualquer chave Pix válida | Garante que a chave pertence a quem diz pertencer |
A geração correta do QR code é responsabilidade da API. A **confirmação do pagamento é responsabilidade da sua aplicação**.

View File

@ -0,0 +1,108 @@
---
title: "Cómo Crear Código QR para WhatsApp"
description: "Aprende a crear un código QR para WhatsApp que permite a los usuarios iniciar una conversación contigo instantáneamente"
keywords: "codigo qr whatsapp, whatsapp codigo qr, qr para whatsapp, crear qr whatsapp"
author: "QR Rapido"
date: 2025-10-08
lastmod: 2025-10-08
image: "/images/tutoriais/whatsapp-qr-hero.jpg"
---
# Cómo Crear Código QR para WhatsApp
Crear un **código QR para WhatsApp** es una de las formas más eficientes de facilitar el contacto directo con tus clientes, amigos o seguidores. Con un simple escaneo, cualquier persona puede iniciar una conversación contigo instantáneamente, sin necesidad de guardar tu número.
## 📱 ¿Por qué usar código QR para WhatsApp?
Los códigos QR para WhatsApp son extremadamente útiles para:
- **Empresas**: Facilitar la atención al cliente
- **Freelancers**: Agilizar el contacto con potenciales clientes
- **Eventos**: Permitir networking rápido
- **Marketing**: Aumentar la conversión en campañas
## 🎯 Paso a Paso
### 1. Prepara tu número
Primero, necesitas tener tu número en formato internacional:
```
595 21 123-4567
```
Elimina todos los espacios y guiones, quedando:
```
59521123456
```
### 2. Crea la URL de WhatsApp
La URL de WhatsApp sigue este patrón:
```
https://wa.me/59521123456
```
Puedes agregar un mensaje predefinido:
```
https://wa.me/59521123456?text=¡Hola!%20Quisiera%20más%20información
```
### 3. Genera el código QR
Accede a [QR Rapido](https://qrrapido.site) y:
1. Selecciona el tipo **URL**
2. Pega la URL de WhatsApp
3. Personaliza los colores (opcional)
4. Haz clic en **Generar Código QR**
5. Descarga en alta calidad
## 💡 Consejos Profesionales
### Personaliza el Mensaje Inicial
Configura un mensaje de bienvenida automático para mejorar la experiencia:
```
https://wa.me/59521123456?text=¡Hola!%20Vine%20a%20través%20de%20tu%20código%20QR
```
### Úsalo en Materiales Impresos
- **Tarjetas de visita**: Facilita el contacto instantáneo
- **Folletos**: Aumenta el engagement
- **Embalajes**: Ofrece soporte directo al cliente
- **Banners**: En eventos y ferias
### Monitorea los Resultados
Para rastrear cuántas personas escanearon tu código QR, considera usar un acortador de URL con analytics antes de generar el QR.
## ⚠️ Cuidados Importantes
1. **Prueba antes de imprimir**: Siempre escanea para verificar que funciona
2. **Tamaño mínimo**: Mantén al menos 3x3 cm para fácil lectura
3. **Contraste adecuado**: Usa colores que contrasten bien (negro sobre blanco es ideal)
4. **Mensaje claro**: Indica para qué sirve el código QR
## 🚀 Ventajas de usar QR Rapido
- ⚡ **Ultra rápido**: Genera en menos de 1 segundo
- 🎨 **Personalizable**: Elige colores y estilos
- 📥 **Alta calidad**: Descarga en PNG, SVG y PDF
- 🔒 **Seguro**: Tus datos no son almacenados
- 💯 **Gratis**: 10 códigos QR por día
## Conclusión
Crear un código QR para WhatsApp es simple y puede revolucionar la forma en que te comunicas con tu público. Con QR Rapido, tienes todo lo que necesitas para crear códigos QR profesionales en segundos.
**¿Listo para empezar?** [Crea tu código QR ahora →](https://qrrapido.site/es-PY)
---
*¿Tienes dudas? [Contáctanos](https://qrrapido.site/es-PY/Contact)!*

View File

@ -0,0 +1,108 @@
---
title: "Cómo Crear Código QR para WhatsApp"
description: "Aprende a crear un código QR para WhatsApp que permite a los usuarios iniciar una conversación contigo instantáneamente"
keywords: "codigo qr whatsapp, whatsapp codigo qr, qr para whatsapp, crear qr whatsapp"
author: "QR Rapido"
date: 2025-10-08
lastmod: 2025-10-08
image: "/images/tutoriais/whatsapp-qr-hero.jpg"
---
# Cómo Crear Código QR para WhatsApp
Crear un **código QR para WhatsApp** es una de las formas más eficientes de facilitar el contacto directo con tus clientes, amigos o seguidores. Con un simple escaneo, cualquier persona puede iniciar una conversación contigo instantáneamente, sin necesidad de guardar tu número.
## 📱 ¿Por qué usar código QR para WhatsApp?
Los códigos QR para WhatsApp son extremadamente útiles para:
- **Empresas**: Facilitar la atención al cliente
- **Freelancers**: Agilizar el contacto con potenciales clientes
- **Eventos**: Permitir networking rápido
- **Marketing**: Aumentar la conversión en campañas
## 🎯 Paso a Paso
### 1. Prepara tu número
Primero, necesitas tener tu número en formato internacional:
```
+1 555 123-4567
```
Elimina todos los espacios y guiones, quedando:
```
15551234567
```
### 2. Crea la URL de WhatsApp
La URL de WhatsApp sigue este patrón:
```
https://wa.me/15551234567
```
Puedes agregar un mensaje predefinido:
```
https://wa.me/15551234567?text=¡Hola!%20Quisiera%20más%20información
```
### 3. Genera el código QR
Accede a [QR Rapido](https://qrrapido.site) y:
1. Selecciona el tipo **URL**
2. Pega la URL de WhatsApp
3. Personaliza los colores (opcional)
4. Haz clic en **Generar Código QR**
5. Descarga en alta calidad
## 💡 Consejos Profesionales
### Personaliza el Mensaje Inicial
Configura un mensaje de bienvenida automático para mejorar la experiencia:
```
https://wa.me/15551234567?text=¡Hola!%20Vine%20a%20través%20de%20tu%20código%20QR
```
### Úsalo en Materiales Impresos
- **Tarjetas de visita**: Facilita el contacto instantáneo
- **Folletos**: Aumenta el engagement
- **Embalajes**: Ofrece soporte directo al cliente
- **Banners**: En eventos y ferias
### Monitorea los Resultados
Para rastrear cuántas personas escanearon tu código QR, considera usar un acortador de URL con analytics antes de generar el QR.
## ⚠️ Cuidados Importantes
1. **Prueba antes de imprimir**: Siempre escanea para verificar que funciona
2. **Tamaño mínimo**: Mantén al menos 3x3 cm para fácil lectura
3. **Contraste adecuado**: Usa colores que contrasten bien (negro sobre blanco es ideal)
4. **Mensaje claro**: Indica para qué sirve el código QR
## 🚀 Ventajas de usar QR Rapido
- ⚡ **Ultra rápido**: Genera en menos de 1 segundo
- 🎨 **Personalizable**: Elige colores y estilos
- 📥 **Alta calidad**: Descarga en PNG, SVG y PDF
- 🔒 **Seguro**: Tus datos no son almacenados
- 💯 **Gratis**: 10 códigos QR por día
## Conclusión
Crear un código QR para WhatsApp es simple y puede revolucionar la forma en que te comunicas con tu público. Con QR Rapido, tienes todo lo que necesitas para crear códigos QR profesionales en segundos.
**¿Listo para empezar?** [Crea tu código QR ahora →](https://qrrapido.site/es)
---
*¿Tienes dudas? [Contáctanos](https://qrrapido.site/es/Contact)!*

View File

@ -0,0 +1,271 @@
---
title: "Cómo Crear Código QR WiFi Gratis: Comparte tu Red en Segundos"
description: "Aprende a crear código QR WiFi gratuito en pocos clics. Comparte la contraseña de tu red sin escribir con nuestro generador rápido y seguro."
keywords: "codigo qr wifi, crear codigo qr wifi, generador qr wifi gratis, qr wifi, compartir contraseña wifi, codigo qr red wifi, como hacer qr wifi"
author: "QR Rapido"
date: 2025-10-10
lastmod: 2025-10-10
image: "/images/tutoriais/qr-code-wifi-hero.jpg"
---
# Cómo Crear Código QR WiFi Gratis: Comparte tu Red en Segundos
¿Cansado de escribir contraseñas largas y complicadas cada vez que un visitante pide el WiFi? Con un **Código QR WiFi**, puedes compartir tu red instantáneamente. En este tutorial completo, aprenderás a crear un código QR para WiFi gratuitamente en menos de 2 minutos.
## ¿Por Qué Usar Código QR para WiFi?
Compartir tu red WiFi mediante código QR ofrece varias ventajas:
- **Practicidad**: Los visitantes se conectan instantáneamente sin escribir contraseñas
- **Seguridad**: No necesitas decir la contraseña en voz alta o anotarla
- **Profesionalismo**: Ideal para empresas, cafeterías, restaurantes y consultorios
- **Ahorro de tiempo**: Elimina errores de escritura y pedidos repetidos
- **Compatibilidad**: Funciona en prácticamente todos los smartphones modernos
## Paso a Paso: Cómo Crear tu Código QR WiFi
### Paso 1: Selecciona el Tipo de Código QR
Accede al generador y elige la opción **WiFi** en la lista de tipos de códigos QR disponibles.
**[INSERTAR IMAGEN 1 AQUÍ: Pantalla de selección de tipo de código QR con WiFi destacado]**
En el menú desplegable encontrarás varias opciones como URL/Link, Texto Simple, Tarjeta de Visita, SMS y Email. Para este tutorial, selecciona **WiFi**.
### Paso 2: Completa los Datos de tu Red WiFi
Ahora necesitas informar los datos de tu red. Ve los campos obligatorios:
**[INSERTAR IMAGEN 3 AQUÍ: Formulario de creación de código QR WiFi con campos completados]**
#### NetworkName (Nombre de la Red) *
Escribe exactamente el nombre de tu red WiFi (SSID). Por ejemplo: "NombreDeTuRed"
**Consejo importante**: El nombre debe ser idéntico al que aparece cuando buscas redes WiFi en el celular. ¡Respeta mayúsculas y minúsculas!
#### SecurityType (Tipo de Seguridad)
Selecciona el tipo de encriptación de tu red:
- **Red WPA (la más común)**: WPA/WPA2/WPA3 - recomendado y más seguro
- **WEP (muy antiguo)**: No recomendado por ser inseguro
- **Sin contraseña**: Para redes públicas sin protección
**[INSERTAR IMAGEN 4 AQUÍ: Ejemplo de formulario WiFi completado con "Rede-do-meu-comercio"]**
#### NetworkPassword (Contraseña de la Red) *
Introduce la contraseña de tu red WiFi. Usa el ícono de ojo para visualizar y verificar que escribiste correctamente.
**Importante**: La contraseña también distingue mayúsculas de minúsculas. ¡Verifica con atención!
#### HiddenNetwork (Red Oculta)
Marca esta opción solo si tu red está configurada como oculta (no aparece en la lista de redes disponibles).
### Paso 3: Personaliza tu Código QR (Opcional)
¡Dale a tu código QR la identidad de tu negocio!
**[INSERTAR IMAGEN 2 AQUÍ: Panel de personalización avanzada]**
#### Opciones de Personalización:
**Color Principal**: Elige el color de los cuadrados del código QR (predeterminado: azul)
**Color de Fondo**: Define el color de fondo (predeterminado: blanco)
**Tamaño**: Selecciona entre:
- Pequeño (200px) - Para uso digital
- Mediano (300px) - Recomendado para impresión
- Grande (500px) - Para banners y pósters
**Margen**:
- Compacto - Ocupa menos espacio
- Normal - Recomendado (mejor lectura)
- Amplio - Para impresiones grandes
**Consejo de diseño**: Mantén buen contraste entre el color principal y el fondo para garantizar que todos los celulares puedan leer el código.
### Paso 4: Generar y Descargar
Haz clic en el botón azul **"⚡ Generar Código QR Rápidamente"** y ¡listo! Tu código QR WiFi será generado instantáneamente.
Puedes:
- Descargar la imagen en alta calidad
- Imprimir y colocar en lugares visibles
- Compartir digitalmente
- Guardar para usar después
## Cómo tus Visitantes Usarán el Código QR WiFi
¡Es muy simple! Tus visitantes solo necesitan:
1. Abrir la cámara del celular (iOS o Android)
2. Apuntar al código QR
3. Tocar la notificación que aparece
4. Conectarse automáticamente al WiFi
**¡No necesitas descargar aplicaciones!** La mayoría de los smartphones desde 2018 ya tienen lectores de códigos QR integrados en la cámara.
## Dónde Usar tu Código QR WiFi
### Para Empresas
- Recepción de oficinas
- Salas de reuniones
- Áreas de espera
- Espacios de coworking
### Para Comercios
- Mesas de restaurantes
- Mostradores de cafeterías
- Tiendas y boutiques
- Salones de belleza
### Para Residencias
- Cuadro de entrada
- Área de parrilla
- Heladera (para fiestas)
- Home office
### Para Eventos
- Credenciales de eventos
- Stands de ferias
- Conferencias
- Bodas y fiestas
## Consejos de Seguridad
⚠️ **Importante**: Considera crear una red WiFi separada para visitantes (red guest) si deseas:
- Proteger tus dispositivos personales
- Limitar velocidad para invitados
- Tener control sobre quién accede
- Mantener tu red principal privada
Muchos routers modernos permiten crear redes guest fácilmente en las configuraciones.
## Preguntas Frecuentes (FAQ)
### ¿El Código QR WiFi expira?
¡No! El código QR funciona indefinidamente mientras los datos de la red (nombre y contraseña) permanezcan iguales.
### ¿Funciona en iPhone y Android?
¡Sí! Funciona en prácticamente todos los smartphones fabricados después de 2018 que tienen cámara.
### ¿Puedo crear para red 5GHz?
¡Sí! El proceso es exactamente el mismo. Solo usa el nombre correcto de la red 5GHz.
### ¿Es seguro?
¡Sí! El código QR solo facilita la escritura de los datos. Es tan seguro como informar la contraseña verbalmente o por escrito.
### ¿Puedo editar después de creado?
No es posible editar el código QR después de generado. Si cambias la contraseña del WiFi, necesitarás generar un nuevo código QR.
### ¿Cuántas personas pueden usar el mismo código QR?
¡Ilimitadas! No hay límite de usos para el código QR.
## Casos de Uso Reales
### Restaurantes y Cafeterías
En Paraguay, muchos negocios gastronómicos están adoptando códigos QR WiFi. Los clientes aprecian poder conectarse sin interrumpir al personal. Coloca el código QR en:
- Carteles en las mesas
- Menús impresos
- Pared cerca de la caja
### Consultorios Médicos
Los pacientes en la sala de espera pueden conectarse fácilmente mientras aguardan. Esto mejora la experiencia y reduce el estrés de la espera.
### Oficinas y Coworking
Facilita el acceso de clientes, proveedores y visitantes sin comprometer la seguridad de tu red principal. Ideal para espacios colaborativos en Asunción y otras ciudades.
### Hoteles y Hospedajes
Proporciona códigos QR en las habitaciones para que los huéspedes se conecten inmediatamente al llegar.
## Errores Comunes a Evitar
### ❌ Error 1: Nombre de Red Incorrecto
Verifica que escribiste exactamente el SSID de tu red. Un error común es confundir la red 2.4GHz con la 5GHz.
### ❌ Error 2: Contraseña con Espacios
Si tu contraseña tiene espacios, inclúyelos exactamente como están configurados en el router.
### ❌ Error 3: Tipo de Seguridad Incorrecto
Asegúrate de seleccionar el tipo correcto (WPA, WEP o sin contraseña). Si no estás seguro, verifica en la configuración de tu router.
### ❌ Error 4: QR Code Muy Pequeño
Para impresión, usa tamaño mediano (300px) o grande (500px). Los códigos pequeños pueden ser difíciles de escanear.
### ❌ Error 5: Bajo Contraste
Evita combinaciones de colores como amarillo sobre blanco o gris claro sobre gris. El contraste es esencial para la lectura correcta.
## Mejores Prácticas para Imprimir
Si vas a imprimir tu código QR WiFi, sigue estas recomendaciones:
### Material Recomendado
- **Papel fotográfico**: Para mejor calidad y durabilidad
- **Laminado**: Protege contra humedad y suciedad
- **Acrílico**: Solución premium para negocios
- **Vinilo adhesivo**: Fácil de colocar en paredes y superficies
### Tamaño de Impresión
- **Mínimo**: 5x5 cm para lectura cercana
- **Recomendado**: 10x10 cm para lectura a 30-50 cm de distancia
- **Grande**: 15x15 cm o más para lectura a mayor distancia
### Ubicación Estratégica
- A la altura de los ojos (1.40m - 1.60m)
- Bien iluminado
- Sin reflejos o brillos
- Fácilmente visible desde donde la gente se sienta o espera
## Tips para Negocios
### Agrega un Texto Atractivo
No solo coloques el código QR. Agrega texto como:
- "WiFi Gratis - Escanea y Conecta"
- "Internet Rápido - Solo Escanea"
- "Conectate Fácil con un Click"
### Diseño Personalizado
Usa los colores de tu marca en el código QR para mantener la coherencia visual con tu negocio.
### Múltiples Ubicaciones
En locales grandes, coloca varios códigos QR en diferentes puntos para facilitar el acceso.
### Actualización Periódica
Por seguridad, considera cambiar la contraseña WiFi cada 3-6 meses y generar un nuevo código QR.
## Conclusión
Crear un código QR WiFi es rápido, fácil y completamente gratuito en QR Rapido. En menos de 2 minutos puedes:
✅ Generar tu código QR personalizado
✅ Compartir tu red sin esfuerzo
✅ Proporcionar mejor experiencia a los visitantes
✅ Demostrar profesionalismo
**Prueba ahora mismo**: [Crear mi Código QR WiFi Gratis](/)
---
**¿Te gustó este tutorial?** ¡Comparte con amigos que también quieren facilitar el acceso a la red WiFi! Explora también nuestros otros tipos de códigos QR para diferentes necesidades.
## Otros Tutoriales Útiles
- Cómo crear código QR para WhatsApp
- Código QR para Tarjeta de Visita Digital
- Cómo crear código QR para URLs y Links
- Personalización avanzada de códigos QR
**¡Crea ahora tu código QR WiFi gratuito y transforma la experiencia de tus visitantes!** 🚀📱
---
## Soporte Técnico
¿Tienes problemas para crear tu código QR WiFi? Contáctanos:
- **Email**: soporte@qrrapido.site
- **WhatsApp**: [Agregar número]
- **Horario**: Lunes a Viernes, 8:00 - 18:00 (hora de Paraguay)
¡Estamos aquí para ayudarte a crear el código QR perfecto para tu negocio o hogar!

View File

@ -0,0 +1,271 @@
---
title: "Cómo Crear Código QR WiFi Gratis: Comparte tu Red en Segundos"
description: "Aprende a crear código QR WiFi gratuito en pocos clics. Comparte la contraseña de tu red sin escribir con nuestro generador rápido y seguro."
keywords: "codigo qr wifi, crear codigo qr wifi, generador qr wifi gratis, qr wifi, compartir contraseña wifi, codigo qr red wifi, como hacer qr wifi"
author: "QR Rapido"
date: 2025-10-10
lastmod: 2025-10-10
image: "/images/tutoriais/qr-code-wifi-hero.jpg"
---
# Cómo Crear Código QR WiFi Gratis: Comparte tu Red en Segundos
¿Cansado de escribir contraseñas largas y complicadas cada vez que un visitante pide el WiFi? Con un **Código QR WiFi**, puedes compartir tu red instantáneamente. En este tutorial completo, aprenderás a crear un código QR para WiFi gratuitamente en menos de 2 minutos.
## ¿Por Qué Usar Código QR para WiFi?
Compartir tu red WiFi mediante código QR ofrece varias ventajas:
- **Practicidad**: Los visitantes se conectan instantáneamente sin escribir contraseñas
- **Seguridad**: No necesitas decir la contraseña en voz alta o anotarla
- **Profesionalismo**: Ideal para empresas, cafeterías, restaurantes y consultorios
- **Ahorro de tiempo**: Elimina errores de escritura y pedidos repetidos
- **Compatibilidad**: Funciona en prácticamente todos los smartphones modernos
## Paso a Paso: Cómo Crear tu Código QR WiFi
### Paso 1: Selecciona el Tipo de Código QR
Accede al generador y elige la opción **WiFi** en la lista de tipos de códigos QR disponibles.
**[INSERTAR IMAGEN 1 AQUÍ: Pantalla de selección de tipo de código QR con WiFi destacado]**
En el menú desplegable encontrarás varias opciones como URL/Link, Texto Simple, Tarjeta de Visita, SMS y Email. Para este tutorial, selecciona **WiFi**.
### Paso 2: Completa los Datos de tu Red WiFi
Ahora necesitas informar los datos de tu red. Ve los campos obligatorios:
**[INSERTAR IMAGEN 3 AQUÍ: Formulario de creación de código QR WiFi con campos completados]**
#### NetworkName (Nombre de la Red) *
Escribe exactamente el nombre de tu red WiFi (SSID). Por ejemplo: "NombreDeTuRed"
**Consejo importante**: El nombre debe ser idéntico al que aparece cuando buscas redes WiFi en el celular. ¡Respeta mayúsculas y minúsculas!
#### SecurityType (Tipo de Seguridad)
Selecciona el tipo de encriptación de tu red:
- **Red WPA (la más común)**: WPA/WPA2/WPA3 - recomendado y más seguro
- **WEP (muy antiguo)**: No recomendado por ser inseguro
- **Sin contraseña**: Para redes públicas sin protección
**[INSERTAR IMAGEN 4 AQUÍ: Ejemplo de formulario WiFi completado con "Rede-do-meu-comercio"]**
#### NetworkPassword (Contraseña de la Red) *
Introduce la contraseña de tu red WiFi. Usa el ícono de ojo para visualizar y verificar que escribiste correctamente.
**Importante**: La contraseña también distingue mayúsculas de minúsculas. ¡Verifica con atención!
#### HiddenNetwork (Red Oculta)
Marca esta opción solo si tu red está configurada como oculta (no aparece en la lista de redes disponibles).
### Paso 3: Personaliza tu Código QR (Opcional)
¡Dale a tu código QR la identidad de tu negocio!
**[INSERTAR IMAGEN 2 AQUÍ: Panel de personalización avanzada]**
#### Opciones de Personalización:
**Color Principal**: Elige el color de los cuadrados del código QR (predeterminado: azul)
**Color de Fondo**: Define el color de fondo (predeterminado: blanco)
**Tamaño**: Selecciona entre:
- Pequeño (200px) - Para uso digital
- Mediano (300px) - Recomendado para impresión
- Grande (500px) - Para banners y pósters
**Margen**:
- Compacto - Ocupa menos espacio
- Normal - Recomendado (mejor lectura)
- Amplio - Para impresiones grandes
**Consejo de diseño**: Mantén buen contraste entre el color principal y el fondo para garantizar que todos los celulares puedan leer el código.
### Paso 4: Generar y Descargar
Haz clic en el botón azul **"⚡ Generar Código QR Rápidamente"** y ¡listo! Tu código QR WiFi será generado instantáneamente.
Puedes:
- Descargar la imagen en alta calidad
- Imprimir y colocar en lugares visibles
- Compartir digitalmente
- Guardar para usar después
## Cómo tus Visitantes Usarán el Código QR WiFi
¡Es muy simple! Tus visitantes solo necesitan:
1. Abrir la cámara del celular (iOS o Android)
2. Apuntar al código QR
3. Tocar la notificación que aparece
4. Conectarse automáticamente al WiFi
**¡No necesitas descargar aplicaciones!** La mayoría de los smartphones desde 2018 ya tienen lectores de códigos QR integrados en la cámara.
## Dónde Usar tu Código QR WiFi
### Para Empresas
- Recepción de oficinas
- Salas de reuniones
- Áreas de espera
- Espacios de coworking
### Para Comercios
- Mesas de restaurantes
- Mostradores de cafeterías
- Tiendas y boutiques
- Salones de belleza
### Para Residencias
- Cuadro de entrada
- Área de parrilla
- Heladera (para fiestas)
- Home office
### Para Eventos
- Credenciales de eventos
- Stands de ferias
- Conferencias
- Bodas y fiestas
## Consejos de Seguridad
⚠️ **Importante**: Considera crear una red WiFi separada para visitantes (red guest) si deseas:
- Proteger tus dispositivos personales
- Limitar velocidad para invitados
- Tener control sobre quién accede
- Mantener tu red principal privada
Muchos routers modernos permiten crear redes guest fácilmente en las configuraciones.
## Preguntas Frecuentes (FAQ)
### ¿El Código QR WiFi expira?
¡No! El código QR funciona indefinidamente mientras los datos de la red (nombre y contraseña) permanezcan iguales.
### ¿Funciona en iPhone y Android?
¡Sí! Funciona en prácticamente todos los smartphones fabricados después de 2018 que tienen cámara.
### ¿Puedo crear para red 5GHz?
¡Sí! El proceso es exactamente el mismo. Solo usa el nombre correcto de la red 5GHz.
### ¿Es seguro?
¡Sí! El código QR solo facilita la escritura de los datos. Es tan seguro como informar la contraseña verbalmente o por escrito.
### ¿Puedo editar después de creado?
No es posible editar el código QR después de generado. Si cambias la contraseña del WiFi, necesitarás generar un nuevo código QR.
### ¿Cuántas personas pueden usar el mismo código QR?
¡Ilimitadas! No hay límite de usos para el código QR.
## Casos de Uso Reales
### Restaurantes y Cafeterías
Muchos negocios gastronómicos están adoptando códigos QR WiFi. Los clientes aprecian poder conectarse sin interrumpir al personal. Coloca el código QR en:
- Carteles en las mesas
- Menús impresos
- Pared cerca de la caja
### Consultorios Médicos
Los pacientes en la sala de espera pueden conectarse fácilmente mientras aguardan. Esto mejora la experiencia y reduce el estrés de la espera.
### Oficinas y Coworking
Facilita el acceso de clientes, proveedores y visitantes sin comprometer la seguridad de tu red principal. Ideal para espacios colaborativos.
### Hoteles y Hospedajes
Proporciona códigos QR en las habitaciones para que los huéspedes se conecten inmediatamente al llegar.
## Errores Comunes a Evitar
### ❌ Error 1: Nombre de Red Incorrecto
Verifica que escribiste exactamente el SSID de tu red. Un error común es confundir la red 2.4GHz con la 5GHz.
### ❌ Error 2: Contraseña con Espacios
Si tu contraseña tiene espacios, inclúyelos exactamente como están configurados en el router.
### ❌ Error 3: Tipo de Seguridad Incorrecto
Asegúrate de seleccionar el tipo correcto (WPA, WEP o sin contraseña). Si no estás seguro, verifica en la configuración de tu router.
### ❌ Error 4: QR Code Muy Pequeño
Para impresión, usa tamaño mediano (300px) o grande (500px). Los códigos pequeños pueden ser difíciles de escanear.
### ❌ Error 5: Bajo Contraste
Evita combinaciones de colores como amarillo sobre blanco o gris claro sobre gris. El contraste es esencial para la lectura correcta.
## Mejores Prácticas para Imprimir
Si vas a imprimir tu código QR WiFi, sigue estas recomendaciones:
### Material Recomendado
- **Papel fotográfico**: Para mejor calidad y durabilidad
- **Laminado**: Protege contra humedad y suciedad
- **Acrílico**: Solución premium para negocios
- **Vinilo adhesivo**: Fácil de colocar en paredes y superficies
### Tamaño de Impresión
- **Mínimo**: 5x5 cm para lectura cercana
- **Recomendado**: 10x10 cm para lectura a 30-50 cm de distancia
- **Grande**: 15x15 cm o más para lectura a mayor distancia
### Ubicación Estratégica
- A la altura de los ojos (1.40m - 1.60m)
- Bien iluminado
- Sin reflejos o brillos
- Fácilmente visible desde donde la gente se sienta o espera
## Tips para Negocios
### Agrega un Texto Atractivo
No solo coloques el código QR. Agrega texto como:
- "WiFi Gratis - Escanea y Conecta"
- "Internet Rápido - Solo Escanea"
- "Conéctate fácil con un clic"
### Diseño Personalizado
Usa los colores de tu marca en el código QR para mantener la coherencia visual con tu negocio.
### Múltiples Ubicaciones
En locales grandes, coloca varios códigos QR en diferentes puntos para facilitar el acceso.
### Actualización Periódica
Por seguridad, considera cambiar la contraseña WiFi cada 3-6 meses y generar un nuevo código QR.
## Conclusión
Crear un código QR WiFi es rápido, fácil y completamente gratuito en QR Rapido. En menos de 2 minutos puedes:
✅ Generar tu código QR personalizado
✅ Compartir tu red sin esfuerzo
✅ Proporcionar mejor experiencia a los visitantes
✅ Demostrar profesionalismo
**Prueba ahora mismo**: [Crear mi Código QR WiFi Gratis](/)
---
**¿Te gustó este tutorial?** ¡Comparte con amigos que también quieren facilitar el acceso a la red WiFi! Explora también nuestros otros tipos de códigos QR para diferentes necesidades.
## Otros Tutoriales Útiles
- Cómo crear código QR para WhatsApp
- Código QR para Tarjeta de Visita Digital
- Cómo crear código QR para URLs y Links
- Personalización avanzada de códigos QR
**¡Crea ahora tu código QR WiFi gratuito y transforma la experiencia de tus visitantes!** 🚀📱
---
## Soporte Técnico
¿Tienes problemas para crear tu código QR WiFi? Contáctanos:
- **Email**: soporte@qrrapido.site
- **WhatsApp**: [Agregar número]
- **Horario**: Lunes a Viernes, 8:00 - 18:00
¡Estamos aquí para ayudarte a crear el código QR perfecto para tu negocio o hogar!

View File

@ -0,0 +1,89 @@
---
title: "How to Create a Static PIX QR Code"
description: "Learn how to create a static PIX QR Code to receive instant payments easily and securely. Complete guide with QR Rapido."
keywords: "pix qr code, generate pix qr code, static pix, create payment qr code, pix qr code free, pix qr code no fee"
author: "QR Rapido"
date: 2026-01-24
lastmod: 2026-01-24
image: "/images/tutoriais/pix-qr-hero.jpg"
---
# How to Create a Static PIX QR Code: The Complete Guide
**PIX** revolutionized the way payments are made in Brazil. And for those who sell products or receive donations, the **PIX QR Code** is an essential tool. In this tutorial, you will learn how to generate a **Static PIX QR Code** for free using **QR Rapido**, ensuring speed and security in your transactions.
## 💸 What is a Static PIX QR Code?
The PIX Static QR Code is ideal for those who want to receive multiple payments of the same amount or varying amounts using a single code. It always "points" to the same bank account (your PIX key) and can contain additional information such as the recipient's name and city.
**Main advantages:**
- **Does not expire:** Use the same code indefinitely.
- **No fees:** Generation is free and does not depend on intermediaries (gateways).
- **Versatile:** Can have a fixed value set or leave the amount open for the payer to fill in.
- **Ideal for:** Merchants, freelancers, donations, crowdfunding and service providers.
## 🎯 Step by Step to Generate on QR Rapido
**QR Rapido** now has a native and secure tool to generate your PIX code. Follow the steps:
### 1. Access the Generator
Open [QR Rapido](https://qrrapido.site) and in the QR Code type menu, select the **"💸 PIX"** option.
### 2. Fill in the Required Data
For the code to work at any bank, the Central Bank standard requires three pieces of information:
- **PIX Key:** Can be your CPF, CNPJ, Email, Phone or Random Key.
- **Beneficiary Name:** Your name or your company's name (must be the same as on the bank account).
- **City:** The city where the account was opened or where you reside.
> **⚠️ Note:** Fill in the data exactly as registered at your bank to avoid errors during payment.
### 3. Set Amount and Description (Optional)
- **Amount:** If you sell a product with a fixed price (e.g., "Coffee $3.00"), fill in the amount field. If it's a donation or variable payment, leave it blank so the customer can enter the amount.
- **Transaction ID (TxID):** An optional code to help you identify the payment in your statement (e.g., "ORDER01"). If not filled, the system will use the default `***`.
- **Description:** A message that may appear on the payer's confirmation screen (e.g., "Service Payment").
### 4. Generate and Customize
Click **"Generate QR Code"**. You can customize the color and style (rounded corners, for example) to match your brand, but remember: **Keep high contrast** (preferably black on white) to ensure any phone can read it.
### 5. Download
Download the image in **PNG** for use on social media or **PDF/SVG** to print in high quality on signs and posters.
## 🏪 Tip for Merchants and Sellers
If you sell **many different products** and want to speed up payment at the checkout or on shelves, QR Rapido has the ideal solution:
> **Subscribe to our Monthly Plan!**
> With the Premium plan, you can create and manage an **exclusive QR Code for each product** in your catalog. That way, the customer scans the specific product's code and the correct amount is already filled in, avoiding errors and speeding up the sale. [Learn more about the Premium plan](/Pagamento/SelecaoPlano).
## 💡 Professional Tips for Your PIX
### Print with Quality
If you're going to place the QR Code on your store counter, print it at a readable size (at least 2" x 2"). Protect the paper with lamination or use an acrylic display.
### Test Before Distributing
Before printing 1000 flyers or posting on Instagram, do a real test! Open your bank app, read the generated QR Code and transfer a symbolic amount to make sure the data is correct and the money goes to the right account.
### Security
QR Rapido generates the code directly in your browser. We do **not** have access to your bank account and do **not** intermediate the money. The payment goes directly from the customer to your account.
## 🚀 Why use QR Rapido for PIX?
- **EMVCo Compliance:** We use the official international standard, guaranteeing compatibility with all major banking apps.
- **Privacy:** Your sensitive data is not stored.
- **Speed:** Generate your code in seconds, without lengthy registrations.
## Conclusion
Having a PIX QR Code on hand speeds up service and conveys professionalism. With **QR Rapido**, you create yours for free, customize it and start receiving payments right away.
**Ready to receive payments?** [Generate your PIX QR Code now →](https://qrrapido.site)
---
*Questions about generation? [Contact us](https://qrrapido.site/en/Contact) or check our FAQ.*

View File

@ -0,0 +1,89 @@
---
title: "Como Criar QR Code PIX Estático"
description: "Aprenda a criar um QR Code PIX estático para receber pagamentos instantâneos de forma fácil e segura. Guia completo com o QR Rapido."
keywords: "qr code pix, gerar qr code pix, pix estático, criar qr code pagamento, pix qr code gratuito, qr code pix sem taxa"
author: "QR Rapido"
date: 2026-01-24
lastmod: 2026-01-24
image: "/images/tutoriais/pix-qr-hero.jpg"
---
# Como Criar QR Code PIX Estático: O Guia Completo
O **PIX** revolucionou a forma como fazemos pagamentos no Brasil. E para quem vende ou recebe doações, o **QR Code PIX** é a ferramenta essencial. Neste tutorial, você vai aprender como gerar um **QR Code PIX Estático** gratuitamente usando o **QR Rapido**, garantindo agilidade e segurança nas suas transações.
## 💸 O que é o QR Code PIX Estático?
O QR Code Estático do PIX é ideal para quem deseja receber múltiplos pagamentos de um mesmo valor ou valores variados usando um único código. Ele "aponta" sempre para a mesma conta bancária (sua chave PIX) e pode conter informações adicionais como o nome do recebedor e a cidade.
**Principais vantagens:**
- **Não expira:** Use o mesmo código indefinidamente.
- **Sem taxas:** A geração é gratuita e não depende de intermediários (gateways).
- **Versátil:** Pode ter um valor fixo definido ou deixar o valor em aberto para o pagador preencher.
- **Ideal para:** Lojistas, autônomos, doações, vaquinhas e prestadores de serviço.
## 🎯 Passo a Passo para Gerar no QR Rapido
O **QR Rapido** agora possui uma ferramenta nativa e segura para gerar seu código PIX. Siga os passos:
### 1. Acesse o Gerador
Abra o [QR Rapido](https://qrrapido.site) e no menu de tipos de QR Code, selecione a opção **"💸 PIX"**.
### 2. Preencha os Dados Obrigatórios
Para que o código funcione em qualquer banco, o padrão do Banco Central exige três informações:
- **Chave PIX:** Pode ser seu CPF, CNPJ, Email, Celular ou Chave Aleatória.
- **Nome do Beneficiário:** Seu nome ou da sua empresa (deve ser o mesmo da conta bancária).
- **Cidade:** A cidade onde a conta foi aberta ou onde você reside.
> **⚠️ Atenção:** Preencha os dados exatamente como estão cadastrados no seu banco para evitar erros na hora do pagamento.
### 3. Defina Valor e Descrição (Opcional)
- **Valor:** Se você vende um produto de preço fixo (ex: "Pastel R$ 10,00"), preencha o campo de valor. Se for uma doação ou pagamento variável, deixe em branco para que o cliente digite o valor.
- **Identificador (TxID):** Um código opcional para você identificar o pagamento no seu extrato (ex: "RIFA01"). Se não preencher, o sistema usará o padrão `***`.
- **Descrição:** Uma mensagem que pode aparecer na tela de confirmação do pagador (ex: "Pagamento Serviços").
### 4. Gere e Personalize
Clique em **"Gerar QR Code"**. Você pode personalizar a cor e o estilo (cantos arredondados, por exemplo) para combinar com sua marca, mas lembre-se: **Mantenha o contraste alto** (preferencialmente preto no branco) para garantir que qualquer celular consiga ler.
### 5. Faça o Download
Baixe a imagem em **PNG** para usar nas redes sociais ou **PDF/SVG** para imprimir com alta qualidade em placas e cartazes.
## 🏪 Dica para Lojistas e Vendedores
Se você vende **muitos produtos diferentes** e quer agilizar o pagamento no caixa ou nas prateleiras, o QR Rapido tem a solução ideal:
> **Assine nosso Plano Mensal!**
> Com o plano Premium, você pode criar e gerenciar um **QR Code exclusivo para cada produto** do seu catálogo. Assim, o cliente escaneia o código do produto específico e o valor já aparece preenchido corretamente, evitando erros e agilizando a venda. [Saiba mais sobre o plano Premium](/Pagamento/SelecaoPlano).
## 💡 Dicas Profissionais para seu PIX
### Imprima com Qualidade
Se for colocar o QR Code no balcão da sua loja, imprima em tamanho legível (pelo menos 5x5 cm). Proteja o papel com plastificação ou use um display de acrílico.
### Teste Antes de Divulgar
Antes de imprimir 1000 panfletos ou postar no Instagram, faça um teste real! Abra o app do seu banco, leia o QR Code gerado e transfira um valor simbólico (R$ 0,01) para garantir que os dados estão corretos e o dinheiro cai na conta certa.
### Segurança
O QR Rapido gera o código diretamente no seu navegador. Nós **não** temos acesso à sua conta bancária e **não** intermediamos o dinheiro. O pagamento vai direto do cliente para sua conta.
## 🚀 Por que usar o QR Rapido para PIX?
- **Conformidade EMVCo:** Utilizamos o padrão oficial internacional, garantindo compatibilidade com NuBank, Itaú, Bradesco, Inter, Banco do Brasil e todos os outros.
- **Privacidade:** Seus dados sensíveis não são armazenados.
- **Velocidade:** Gere seu código em segundos, sem cadastros demorados.
## Conclusão
Ter um QR Code PIX à mão agiliza o atendimento e passa profissionalismo. Com o **QR Rapido**, você cria o seu de graça, personaliza e já sai recebendo.
**Pronto para receber pagamentos?** [Gere seu QR Code PIX agora →](https://qrrapido.site)
---
*Dúvidas sobre a geração? [Fale conosco](https://qrrapido.site/pt-BR/Contact) ou consulte nosso FAQ.*

View File

@ -0,0 +1,108 @@
---
title: "How to Create a QR Code for WhatsApp"
description: "Learn how to create a WhatsApp QR Code that allows users to instantly start a conversation with you"
keywords: "whatsapp qr code, qr code for whatsapp, create whatsapp qr code, whatsapp qr"
author: "QR Rapido"
date: 2025-10-08
lastmod: 2025-10-08
image: "/images/tutoriais/whatsapp-qr-hero.jpg"
---
# How to Create a QR Code for WhatsApp
Creating a **WhatsApp QR Code** is one of the most efficient ways to facilitate direct contact with your customers, friends or followers. With a simple scan, anyone can instantly start a conversation with you without needing to save your number.
## 📱 Why use a QR Code for WhatsApp?
WhatsApp QR Codes are extremely useful for:
- **Businesses**: Facilitating customer service
- **Freelancers**: Speeding up contact with potential clients
- **Events**: Enabling quick networking
- **Marketing**: Increasing conversion in campaigns
## 🎯 Step by Step
### 1. Prepare your number
First, you need to have your number in international format:
```
1 555 987-6543
```
Remove all spaces and dashes:
```
15559876543
```
### 2. Create the WhatsApp URL
The WhatsApp URL follows this pattern:
```
https://wa.me/15559876543
```
You can add a pre-defined message:
```
https://wa.me/15559876543?text=Hi!%20I%20would%20like%20more%20information
```
### 3. Generate the QR Code
Access [QR Rapido](https://qrrapido.site) and:
1. Select the **URL** type
2. Paste the WhatsApp URL
3. Customize the colors (optional)
4. Click **Generate QR Code**
5. Download in high quality
## 💡 Professional Tips
### Customize the Initial Message
Set up an automatic welcome message to improve the experience:
```
https://wa.me/15559876543?text=Hi!%20I%20found%20you%20through%20your%20QR%20Code
```
### Use on Printed Materials
- **Business cards**: Make instant contact easy
- **Flyers and leaflets**: Increase engagement
- **Packaging**: Offer direct customer support
- **Banners**: At events and trade shows
### Monitor the Results
To track how many people scanned your QR Code, consider using a URL shortener with analytics before generating the QR Code.
## ⚠️ Important Cautions
1. **Test before printing**: Always scan to verify it works
2. **Minimum size**: Keep at least 1.2" x 1.2" for easy reading
3. **Adequate contrast**: Use colors that contrast well (black on white is ideal)
4. **Clear message**: Indicate what the QR Code is for
## 🚀 Advantages of using QR Rapido
- ⚡ **Ultra-fast**: Generate in less than 1 second
- 🎨 **Customizable**: Choose colors and styles
- 📥 **High quality**: Download in PNG, SVG and PDF
- 🔒 **Secure**: Your data is not stored
- 💯 **Free**: up to 50 QR codes per day after login
## Conclusion
Creating a QR Code for WhatsApp is simple and can revolutionize the way you communicate with your audience. With QR Rapido, you have everything you need to create professional QR Codes in seconds.
**Ready to start?** [Create your QR Code now →](https://qrrapido.site/en)
---
*Have questions? [Contact us](https://qrrapido.site/en/Contact)!*

View File

@ -0,0 +1,108 @@
---
title: "Como Criar QR Code para WhatsApp"
description: "Aprenda a criar um QR Code para WhatsApp que permite aos usuários iniciarem uma conversa com você instantaneamente"
keywords: "qr code whatsapp, whatsapp qr code, qr code para whatsapp, criar qr whatsapp"
author: "QR Rapido"
date: 2025-10-08
lastmod: 2025-10-08
image: "/images/tutoriais/whatsapp-qr-hero.jpg"
---
# Como Criar QR Code para WhatsApp
Criar um **QR Code para WhatsApp** é uma das maneiras mais eficientes de facilitar o contato direto com seus clientes, amigos ou seguidores. Com um simples escaneamento, qualquer pessoa pode iniciar uma conversa com você instantaneamente, sem precisar salvar seu número.
## 📱 Por que usar QR Code para WhatsApp?
Os QR Codes para WhatsApp são extremamente úteis para:
- **Empresas**: Facilitar o atendimento ao cliente
- **Freelancers**: Agilizar o contato com potenciais clientes
- **Eventos**: Permitir networking rápido
- **Marketing**: Aumentar a conversão em campanhas
## 🎯 Passo a Passo
### 1. Prepare seu número
Primeiro, você precisa ter seu número no formato internacional:
```
55 11 98765-4321
```
Remova todos os espaços e traços, ficando:
```
5511987654321
```
### 2. Crie a URL do WhatsApp
A URL do WhatsApp segue este padrão:
```
https://wa.me/5511987654321
```
Você pode adicionar uma mensagem pré-definida:
```
https://wa.me/5511987654321?text=Olá!%20Gostaria%20de%20mais%20informações
```
### 3. Gere o QR Code
Acesse [QR Rapido](https://qrrapido.site) e:
1. Selecione o tipo **URL**
2. Cole a URL do WhatsApp
3. Personalize as cores (opcional)
4. Clique em **Gerar QR Code**
5. Faça o download em alta qualidade
## 💡 Dicas Profissionais
### Personalize a Mensagem Inicial
Configure uma mensagem de boas-vindas automática para melhorar a experiência:
```
https://wa.me/5511987654321?text=Olá!%20Vim%20através%20do%20seu%20QR%20Code
```
### Use em Materiais Impressos
- **Cartões de visita**: Facilite o contato instantâneo
- **Flyers e panfletos**: Aumente o engajamento
- **Embalagens**: Ofereça suporte direto ao cliente
- **Banners**: Em eventos e feiras
### Monitore os Resultados
Para rastrear quantas pessoas escanearam seu QR Code, considere usar um encurtador de URL com analytics antes de gerar o QR Code.
## ⚠️ Cuidados Importantes
1. **Teste antes de imprimir**: Sempre escaneie para verificar se funciona
2. **Tamanho mínimo**: Mantenha pelo menos 3x3 cm para fácil leitura
3. **Contraste adequado**: Use cores que contrastem bem (preto no branco é ideal)
4. **Mensagem clara**: Indique para que serve o QR Code
## 🚀 Vantagens de usar QR Rapido
- ⚡ **Ultrarrápido**: Gere em menos de 1 segundo
- 🎨 **Personalizável**: Escolha cores e estilos
- 📥 **Alta qualidade**: Download em PNG, SVG e PDF
- 🔒 **Seguro**: Seus dados não são armazenados
- 💯 **Gratuito**: 10 QR codes por dia
## Conclusão
Criar um QR Code para WhatsApp é simples e pode revolucionar a forma como você se comunica com seu público. Com o QR Rapido, você tem tudo que precisa para criar QR Codes profissionais em segundos.
**Pronto para começar?** [Crie seu QR Code agora →](https://qrrapido.site/pt-BR)
---
*Tem dúvidas? [Entre em contato](https://qrrapido.site/pt-BR/Contact) conosco!*

View File

@ -0,0 +1,179 @@
---
title: "How to Create a Free WiFi QR Code: Share Your Network in Seconds"
description: "Learn how to create a free WiFi QR Code in a few clicks. Share your network password without typing with our fast and secure generator."
keywords: "wifi qr code, create wifi qr code, free wifi qr code generator, wifi network qr code, share wifi password, free wifi qr code, how to make wifi qr code"
author: "QR Rapido"
date: 2025-10-10
lastmod: 2025-10-10
image: "/images/tutoriais/qr-code-wifi-hero.jpg"
---
# How to Create a Free WiFi QR Code: Share Your Network in Seconds
Tired of typing long and complicated passwords every time a guest asks for your WiFi password? With a **WiFi QR Code**, you can share your network instantly! In this complete tutorial, you'll learn how to create a WiFi QR Code for free in less than 2 minutes.
## Why Use a QR Code for WiFi?
Sharing your WiFi network through a QR Code offers several advantages:
- **Convenience**: Guests connect instantly without typing passwords
- **Security**: No need to say the password out loud or write it down
- **Professionalism**: Ideal for businesses, cafes, restaurants and offices
- **Time saving**: Eliminates typing errors and repeated requests
- **Compatibility**: Works on virtually all modern smartphones
## Step by Step: How to Create Your WiFi QR Code
### Step 1: Select the QR Code Type
Access the generator and choose the **WiFi** option from the list of available QR Code types.
In the dropdown menu, you'll find various options like URL/Link, Plain Text, Business Card, SMS and Email. For this tutorial, select **WiFi**.
### Step 2: Fill in Your WiFi Network Details
Now you need to enter your network details. See the required fields:
#### NetworkName (Network Name) *
Type exactly the name of your WiFi network (SSID). For example: "YourNetworkName"
**Important tip**: The name must be identical to what appears when you search for WiFi networks on your phone. It is case-sensitive!
#### SecurityType (Security Type)
Select the encryption type of your network:
- **WPA Network (most common)**: WPA/WPA2/WPA3 - recommended and most secure
- **WEP (very old)**: Not recommended as it is insecure
- **No password**: For public networks without protection
#### NetworkPassword (Network Password) *
Enter your WiFi network password. Use the eye icon to view and verify you typed it correctly.
**Important**: The password is also case-sensitive. Check it carefully!
#### HiddenNetwork (Hidden Network)
Check this option only if your network is configured as hidden (does not appear in the list of available networks).
### Step 3: Customize Your QR Code (Optional)
Give your QR Code your business's look!
#### Customization Options:
**Primary Color**: Choose the color of the QR Code squares (default: black)
**Background Color**: Set the background color (default: white)
**Size**: Choose from:
- Small (200px) - For digital use
- Medium (300px) - Recommended for printing
- Large (500px) - For banners and posters
**Margin**:
- Compact - Takes up less space
- Normal - Recommended (better readability)
- Wide - For large prints
**Design tip**: Maintain good contrast between the primary color and background to ensure all phones can read the code.
### Step 4: Generate and Download
Click the **"⚡ Generate QR Code Quickly"** button and you're done! Your WiFi QR Code will be generated instantly.
You can:
- Download the image in high quality
- Print and place in visible locations
- Share digitally
- Save to use later
## How Your Guests Will Use the WiFi QR Code
It's very simple! Your guests just need to:
1. Open the phone camera (iOS or Android)
2. Point at the QR Code
3. Tap the notification that appears
4. Connect automatically to the WiFi
**No app download needed!** Most smartphones since 2018 already have built-in QR Code readers in the camera.
## Where to Use Your WiFi QR Code
### For Businesses
- Office reception areas
- Meeting rooms
- Waiting areas
- Coworking spaces
### For Retail
- Restaurant tables
- Café counters
- Stores and boutiques
- Beauty salons
### For Homes
- Entry hallway
- Barbecue area
- Refrigerator (for parties)
- Home office
### For Events
- Event credentials
- Trade show booths
- Conferences
- Weddings and parties
## Security Tips
⚠️ **Important**: Consider creating a separate WiFi network for guests (guest network) if you want to:
- Protect your personal devices
- Limit speed for guests
- Have control over who accesses your network
- Keep your main network private
Many modern routers allow you to easily create guest networks in the settings.
## Frequently Asked Questions (FAQ)
### Does the WiFi QR Code expire?
No! The QR Code works indefinitely as long as the network data (name and password) remains the same.
### Does it work on iPhone and Android?
Yes! It works on virtually all smartphones made after 2018 that have a camera.
### Can I create one for a 5GHz network?
Yes! The process is exactly the same. Just use the correct 5GHz network name.
### Is it safe?
Yes! The QR Code simply makes it easier to enter the data. It is just as safe as sharing the password verbally or in writing.
### Can I edit it after it's created?
You cannot edit the QR Code after it's generated. If you change your WiFi password, you'll need to generate a new QR Code.
### How many people can use the same QR Code?
Unlimited! There is no usage limit for the QR Code.
## Conclusion
Creating a WiFi QR Code is quick, easy and completely free at QR Rapido! In less than 2 minutes you can:
✅ Generate your custom QR Code
✅ Share your network effortlessly
✅ Provide a better experience for guests
✅ Demonstrate professionalism
**Try it now**: [Create my Free WiFi QR Code](/)
---
**Liked this tutorial?** Share it with friends who also want to make WiFi access easier! Also explore our other QR Code types for different needs.
## Other Useful Tutorials
- How to create a QR Code for WhatsApp
- QR Code for Digital Business Cards
- How to create a QR Code for URLs and Links
- Advanced QR Code customization
**Create your free WiFi QR Code now and transform your guests' experience!** 🚀📱

View File

@ -0,0 +1,187 @@
---
title: "Como Criar QR Code WiFi Grátis: Compartilhe sua Rede em Segundos"
description: "Aprenda a criar QR Code WiFi gratuito em poucos cliques. Compartilhe a senha da sua rede sem digitar com nosso gerador rápido e seguro."
keywords: "qr code wifi, criar qr code wifi, gerador qr code wifi grátis, qr code rede wifi, compartilhar senha wifi, qr code wifi gratuito, como fazer qr code wifi"
author: "QR Rapido"
date: 2025-10-10
lastmod: 2025-10-10
image: "/images/tutoriais/qr-code-wifi-hero.jpg"
---
# Como Criar QR Code WiFi Grátis: Compartilhe sua Rede em Segundos
Cansado de digitar senhas longas e complicadas toda vez que um visitante pede a senha do WiFi? Com um **QR Code WiFi**, você pode compartilhar sua rede instantaneamente! Neste tutorial completo, você aprenderá a criar um QR Code para WiFi gratuitamente em menos de 2 minutos.
## Por Que Usar QR Code para WiFi?
Compartilhar sua rede WiFi através de QR Code oferece diversas vantagens:
- **Praticidade**: Visitantes conectam-se instantaneamente sem digitar senhas
- **Segurança**: Não precisa falar a senha em voz alta ou anotá-la
- **Profissionalismo**: Ideal para empresas, cafeterias, restaurantes e consultórios
- **Economia de tempo**: Elimina erros de digitação e pedidos repetidos
- **Compatibilidade**: Funciona em praticamente todos os smartphones modernos
## Passo a Passo: Como Criar seu QR Code WiFi
### Passo 1: Selecione o Tipo de QR Code
Acesse o gerador e escolha a opção **WiFi** na lista de tipos de QR Code disponíveis.
**[INSERIR IMAGEM 1 AQUI: Tela de seleção de tipo de QR Code com WiFi destacado]**
No menu dropdown, você encontrará diversas opções como URL/Link, Texto Simples, Cartão de Visita, SMS e Email. Para este tutorial, selecione **WiFi**.
### Passo 2: Preencha os Dados da sua Rede WiFi
Agora você precisa informar os dados da sua rede. Veja os campos obrigatórios:
**[INSERIR IMAGEM 3 AQUI: Formulário de criação de QR Code WiFi com campos preenchidos]**
#### NetworkName (Nome da Rede) *
Digite exatamente o nome da sua rede WiFi (SSID). Por exemplo: "NomeDaSuaRede"
**Dica importante**: O nome deve ser idêntico ao que aparece quando você busca redes WiFi no celular. Respeite maiúsculas e minúsculas!
#### SecurityType (Tipo de Segurança)
Selecione o tipo de criptografia da sua rede:
- **Rede WPA (a mais comum)**: WPA/WPA2/WPA3 - recomendado e mais seguro
- **WEP (muito antigo)**: Não recomendado por ser inseguro
- **Sem senha**: Para redes públicas sem proteção
**[INSERIR IMAGEM 4 AQUI: Exemplo de formulário WiFi preenchido com "Rede-do-meu-comercio"]**
#### NetworkPassword (Senha da Rede) *
Insira a senha da sua rede WiFi. Use o ícone de olho para visualizar e conferir se digitou corretamente.
**Importante**: A senha também diferencia maiúsculas de minúsculas. Confira com atenção!
#### HiddenNetwork (Rede Oculta)
Marque esta opção apenas se sua rede está configurada como oculta (não aparece na lista de redes disponíveis).
### Passo 3: Personalize seu QR Code (Opcional)
Deixe seu QR Code com a cara do seu negócio!
**[INSERIR IMAGEM 2 AQUI: Painel de personalização avançada]**
#### Opções de Personalização:
**Cor Principal**: Escolha a cor dos quadrados do QR Code (padrão: azul)
**Cor de Fundo**: Defina a cor de fundo (padrão: branco)
**Tamanho**: Selecione entre:
- Pequeno (200px) - Para uso digital
- Médio (300px) - Recomendado para impressão
- Grande (500px) - Para banners e pôsteres
**Margem**:
- Compacta - Ocupa menos espaço
- Normal - Recomendado (melhor leitura)
- Larga - Para impressões grandes
**Dica de design**: Mantenha bom contraste entre a cor principal e o fundo para garantir que todos os celulares consigam ler o código.
### Passo 4: Gerar e Baixar
Clique no botão azul **"⚡ Gerar QR Code Rapidamente"** e pronto! Seu QR Code WiFi será gerado instantaneamente.
Você pode:
- Baixar a imagem em alta qualidade
- Imprimir e colocar em locais visíveis
- Compartilhar digitalmente
- Salvar para usar depois
## Como Seus Visitantes Usarão o QR Code WiFi
É muito simples! Seus visitantes só precisam:
1. Abrir a câmera do celular (iOS ou Android)
2. Apontar para o QR Code
3. Tocar na notificação que aparece
4. Conectar automaticamente ao WiFi
**Não precisa baixar aplicativos!** A maioria dos smartphones desde 2018 já possui leitores de QR Code integrados na câmera.
## Onde Usar seu QR Code WiFi
### Para Empresas
- Recepção de escritórios
- Salas de reunião
- Áreas de espera
- Coworking spaces
### Para Comércios
- Mesas de restaurantes
- Balcões de cafeterias
- Lojas e boutiques
- Salões de beleza
### Para Residências
- Quadro de entrada
- Área da churrasqueira
- Geladeira (para festas)
- Home office
### Para Eventos
- Credenciais de eventos
- Stands de feiras
- Conferências
- Casamentos e festas
## Dicas de Segurança
⚠️ **Importante**: Considere criar uma rede WiFi separada para visitantes (rede guest) se você deseja:
- Proteger seus dispositivos pessoais
- Limitar velocidade para convidados
- Ter controle sobre quem acessa
- Manter sua rede principal privada
Muitos roteadores modernos permitem criar redes guest facilmente nas configurações.
## Perguntas Frequentes (FAQ)
### O QR Code WiFi expira?
Não! O QR Code funciona indefinidamente enquanto os dados da rede (nome e senha) permanecerem os mesmos.
### Funciona em iPhone e Android?
Sim! Funciona em praticamente todos os smartphones fabricados após 2018 que possuem câmera.
### Posso criar para rede 5GHz?
Sim! O processo é exatamente o mesmo. Apenas use o nome correto da rede 5GHz.
### É seguro?
Sim! O QR Code apenas facilita a digitação dos dados. É tão seguro quanto informar a senha verbalmente ou por escrito.
### Posso editar depois de criado?
Não é possível editar o QR Code depois de gerado. Se mudar a senha do WiFi, precisará gerar um novo QR Code.
### Quantas pessoas podem usar o mesmo QR Code?
Ilimitadas! Não há limite de usos para o QR Code.
## Conclusão
Criar um QR Code WiFi é rápido, fácil e completamente gratuito no QR Rapido! Em menos de 2 minutos você pode:
✅ Gerar seu QR Code personalizado
✅ Compartilhar sua rede sem esforço
✅ Proporcionar melhor experiência aos visitantes
✅ Demonstrar profissionalismo
**Experimente agora mesmo**: [Criar meu QR Code WiFi Grátis](/)
---
**Gostou deste tutorial?** Compartilhe com amigos que também querem facilitar o acesso à rede WiFi! Explore também nossos outros tipos de QR Code para diferentes necessidades.
## Outros Tutoriais Úteis
- Como criar QR Code para WhatsApp
- QR Code para Cartão de Visita Digital
- Como criar QR Code para URLs e Links
- Personalização avançada de QR Codes
**Crie agora seu QR Code WiFi gratuito e transforme a experiência dos seus visitantes!** 🚀📱

View File

@ -0,0 +1,599 @@
---
title: "Código QR para Corredores de Inmuebles: Guía Completa para Etiquetas y Volantes"
description: "Descubre cómo usar código QR en etiquetas adhesivas, carteles y volantes inmobiliarios. Aumenta tus ventas con tecnología gratuita y profesional."
keywords: "codigo qr corredor inmuebles, etiqueta corredor inmobiliaria, qr inmobiliaria, etiquetas adhesivas corredores, divulgar corredor propiedades, qr cartel se vende"
author: "QR Rapido"
date: 2025-10-10
lastmod: 2025-10-10
image: "/images/tutoriais/qr-code-corretor-imoveis-hero.jpg"
---
# Código QR para Corredores de Inmuebles: Guía Completa para Etiquetas y Volantes
Si eres corredor de inmuebles, sabes que **captar leads calificados** es esencial para cerrar negocios. Imagina transformar tus carteles de "Se Vende", volantes y etiquetas adhesivas en herramientas interactivas que conectan clientes directamente a tu WhatsApp, ficha del inmueble o tarjeta de visita digital - todo esto **gratuitamente** con códigos QR.
En esta guía completa, aprenderás a crear y aplicar códigos QR profesionales en materiales inmobiliarios, aumentando tus conversiones y destacándote de la competencia.
## ¿Por Qué los Corredores de Inmuebles Deben Usar Códigos QR?
### **Ventajas Comprobadas**
- ✅ **Captación 24/7**: Tu cartel trabaja para ti incluso cuando estás durmiendo
- ✅ **Contacto Instantáneo**: Cliente escanea y ya está en tu WhatsApp
- ✅ **Cero Escritura**: Elimina errores al anotar números
- ✅ **Rastreo**: Sabe cuántas personas se interesaron
- ✅ **Profesionalismo**: Demuestra modernidad e innovación
- ✅ **Costo Cero**: Genera códigos QR ilimitados gratuitamente
- ✅ **Tour Virtual**: Lleva al cliente dentro del inmueble virtualmente
### **Estadísticas del Mercado**
Según estudios del sector inmobiliario:
- **78%** de los compradores investigan propiedades por celular
- **65%** prefieren contacto vía WhatsApp en lugar de llamada
- **43%** escanean códigos QR en carteles de inmuebles cuando los ven
- Corredores que usan código QR tienen **35% más leads** mensuales
---
## Dónde Aplicar Códigos QR en Marketing Inmobiliario
### **1. Carteles de "Se Vende" y "Se Alquila"**
**¡El uso más poderoso!** El cartel al frente del inmueble es visto por cientos de personas diariamente.
**Qué colocar en el código QR:**
- Link directo a tu WhatsApp
- vCard con tus contactos completos
- Tour virtual 360° del inmueble
- Ficha técnica detallada (PDF)
- Video del inmueble en YouTube
**Consejo profesional**: Coloca texto llamativo como:
- "Escanea y agenda tu visita AHORA"
- "Tour Virtual - Apunta tu cámara aquí"
- "WhatsApp Directo del Corredor"
### **2. Etiquetas Adhesivas y Tags**
Etiquetas pequeñas (5x5cm hasta 10x10cm) son perfectas para:
**Aplicaciones:**
- Pegar en autos de la inmobiliaria
- Fijar en portones de inmuebles
- Aplicar en vitrinas de locales
- Distribuir en establecimientos asociados
- Colocar en ascensores de edificios
**Ventajas:**
- Bajo costo de impresión
- Fácil distribución masiva
- Pueden ser cambiadas rápidamente
- Óptimas para acciones promocionales
### **3. Volantes y Flyers**
¡Transforma volantes de papel en herramientas digitales!
**Dónde aplicar:**
- Esquina superior derecha (lugar de mayor atención)
- Centro, si es el foco principal
- Reverso, con llamada destacada
**Contenido recomendado:**
- Portafolio digital de inmuebles
- Formulario de registro
- Calculadora de financiamiento
- Lista completa de propiedades disponibles
### **4. Folders y Revistas Inmobiliarias**
Materiales impresos premium merecen códigos QR estratégicos.
**Uso ideal:**
- 1 QR por inmueble destacado
- QR en la portada para portafolio completo
- QR en la contraportada con tus contactos
- QR en cada página con más información
### **5. Tarjetas de Visita**
¡El clásico nunca pasa de moda, pero puede ser mejorado!
**Código QR en la tarjeta permite:**
- Guardar contacto automáticamente (vCard)
- Ver portafolio online
- Agendar reunión directo en la agenda
- Enviar mensaje vía WhatsApp
### **6. Firma de Email**
¡Cada email que envías es una oportunidad!
**Incluye código QR para:**
- Tu vCard completo
- Último lanzamiento inmobiliario
- Evaluación gratuita de inmueble
- Agendamiento de visitas
---
## Paso a Paso: Cómo Crear Código QR para Corredores
Voy a mostrar cómo crear **3 tipos de códigos QR** esenciales para corredores:
### **Tipo 1: Código QR de vCard (Tarjeta de Visita Digital)**
Perfecto para: Tarjetas de visita, firma de email, credenciales
**[INSERTAR IMAGEN 1 AQUÍ: Selección del tipo de código QR - vCard destacado]**
#### Paso 1: Selecciona "Tarjeta de Visita"
Accede al generador y elige la opción **Tarjeta de Visita** en el menú de tipos.
#### Paso 2: Completa tus Datos Profesionales
**Información obligatoria:**
- **Nombre completo**: Juan Silva
- **Cargo**: Corredor de Inmuebles - Matrícula 12345
- **Empresa**: Inmobiliaria Success
- **Teléfono**: +595 21 123-4567
- **Email**: juan.silva@inmuebles.com.py
- **Website**: www.juansilva.inmuebles.py
- **Dirección**: Av. Mariscal López - Asunción, Paraguay
**Campos opcionales estratégicos:**
- WhatsApp Business
- Instagram profesional
- LinkedIn
- Canal de YouTube con tours virtuales
#### Paso 3: Genera y Descarga
Haz clic en **"Generar Código QR"** y descarga en alta resolución.
**Dónde usar este QR:**
- Etiquetas adhesivas en el auto
- Tarjetas de visita
- Firma de email
- Credencial profesional
---
### **Tipo 2: Código QR para WhatsApp Directo**
Perfecto para: Carteles de inmuebles, volantes, anuncios
**[INSERTAR IMAGEN 3 AQUÍ: Formulario de WhatsApp con código QR completado]**
#### Paso 1: Selecciona "WhatsApp"
En el generador, elige la opción **WhatsApp** (o SMS si prefieres).
#### Paso 2: Configura el Mensaje Pre-llenado
**Ejemplo de mensaje eficaz:**
```
¡Hola! Vi el cartel del inmueble en [CALLE/BARRIO] y me gustaría agendar una visita. ¿Puede pasarme más información?
```
**Por qué el mensaje pre-llenado funciona:**
- Cliente no necesita pensar qué escribir
- Ya sabes de qué inmueble está hablando
- Aumenta en 80% la tasa de conversión
#### Paso 3: Personaliza para Cada Inmueble
**Consejo importante**: ¡Crea códigos QR diferentes para cada inmueble!
Ejemplo para apartamento en Recoleta:
```
¡Hola! Vi el cartel del Departamento 3 dormitorios en Recoleta (Ref: DEPT-001). Me gustaría saber más detalles y agendar visita.
```
**Ventaja**: ¡Ya sabes exactamente qué inmueble quiere ver el cliente!
---
### **Tipo 3: Código QR para URL (Tour Virtual / Ficha Técnica)**
Perfecto para: Inmuebles de alto estándar, lanzamientos, propiedades rurales
**[INSERTAR IMAGEN 4 AQUÍ: Ejemplo de formulario URL completado]**
#### Paso 1: Prepara el Contenido Digital
Antes de crear el QR, necesitas tener:
**Opción A - Tour Virtual:**
- Video en YouTube del inmueble
- Tour 360° (Google Street View, Matterport)
- Galería de fotos en Instagram/Facebook
**Opción B - Landing Page:**
- Ficha técnica completa del inmueble
- Fotos en alta resolución
- Mapa de ubicación
- Calculadora de financiamiento
- Formulario de interés
#### Paso 2: Acorta la URL (¡Importante!)
Usa acortadores como:
- bit.ly
- tinyurl.com
- QR Rapido (si tiene función de acortamiento)
**Ejemplo:**
- ❌ URL larga: `https://www.miinmobiliaria.com.py/inmuebles/departamento-3-dormitorios-recoleta-asuncion-ref-dept001?utm_source=cartel`
- ✅ URL corta: `bit.ly/dept-recoleta-001`
**Ventaja de URL corta**: Genera código QR más simple y fácil de escanear
#### Paso 3: Crea el Código QR
Pega la URL corta en el campo **URL/Link** y genera el código.
---
## Cómo Personalizar tu Código QR Profesionalmente
**[INSERTAR IMAGEN 2 AQUÍ: Panel de personalización con colores personalizados]**
### **Elección de Colores Estratégicos**
**Para Inmobiliarias Tradicionales:**
- Azul marino + Blanco (confianza, seriedad)
- Negro + Dorado (lujo, exclusividad)
- Verde oscuro + Blanco (crecimiento, estabilidad)
**Para Inmobiliarias Modernas:**
- Naranja + Blanco (energía, innovación)
- Púrpura + Blanco (creatividad, diferenciación)
- Rojo + Blanco (urgencia, acción)
**Regla de oro**: ¡Siempre mantén alto contraste entre color principal y fondo!
### **Tamaños Recomendados por Aplicación**
**Carteles de calle (distancia 2-5 metros):**
- Código QR: 15x15cm o mayor
- Resolución: 500px mínimo
**Volantes A5/A4:**
- Código QR: 4x4cm a 6x6cm
- Resolución: 300px
**Etiquetas adhesivas:**
- Código QR: 5x5cm (tamaño del adhesivo)
- Resolución: 300px
**Tarjetas de visita:**
- Código QR: 2,5x2,5cm
- Resolución: 200px
### **Agrega Llamadas a la Acción (CTA)**
¡Nunca coloques solo el código QR! Agrega texto atractivo:
**Ejemplos eficaces:**
- 📱 "Apunta la cámara y habla conmigo en WhatsApp"
- 🏠 "Tour Virtual 360° - Escanea Aquí"
- 💬 "Agenda tu Visita Ahora"
- 📋 "Ve Fotos y Detalles Completos"
- 🎯 "Guarda Mi Contacto Automáticamente"
---
## Estrategias Avanzadas para Corredores
### **1. Códigos QR Rastreables (Dinámicos)**
Usa servicios de código QR dinámico para:
- Saber cuántas personas escanearon
- Ver horario de los escaneos
- Identificar ubicación aproximada
- Cambiar el destino sin reimprimir
**Cuándo usar:**
- Campañas con muchos materiales impresos
- Pruebas A/B de diferentes enfoques
- Carteles permanentes en inmuebles
### **2. Múltiples Códigos QR en el Mismo Cartel**
¿Cartel grande? ¡Usa 2-3 códigos QR diferentes!
**Ejemplo de cartel completo:**
- **QR 1** (arriba): "Habla en WhatsApp"
- **QR 2** (centro): "Tour Virtual 360°"
- **QR 3** (abajo): "Guarda Mi Contacto"
**Ventaja**: Cliente elige la acción que prefiere hacer
### **3. Código QR + Realidad Aumentada**
Para lanzamientos y emprendimientos:
- Código QR lleva a app de RA
- Cliente apunta celular y ve el edificio terminado
- Visualiza departamento decorado
- ¡Extremadamente impactante!
### **4. Campañas de Temporada**
Crea códigos QR específicos para:
- **Enero**: "Planifica 2025 - Compra tu inmueble"
- **Junio**: "Vacaciones de Julio en tu Nuevo Hogar"
- **Noviembre**: "Black Friday Inmobiliaria"
- **Diciembre**: "Empieza el Año en Casa Propia"
### **5. Alianzas Estratégicas**
Distribuye etiquetas con código QR en:
- Tiendas de materiales de construcción
- Oficinas de arquitectura
- Gestorías y escribanías
- Gimnasios y restaurantes del barrio
- Edificios comerciales (tablero de avisos)
---
## Modelos de Etiquetas Adhesivas Profesionales
### **Modelo 1: Etiqueta Minimalista (5x5cm)**
```
┌─────────────────┐
│ [CÓDIGO QR] │
│ │
│ Juan Silva │
│ Matrícula 12345 │
│ (021) 123-4567 │
└─────────────────┘
```
### **Modelo 2: Etiqueta con Destaque (7x7cm)**
```
┌─────────────────────┐
│ VENDE TU INMUEBLE │
│ SIN BUROCRACIA │
│ │
│ [CÓDIGO QR] │
│ │
│ "Escanea y habla │
│ directo conmigo" │
│ │
│ Juan Silva │
│ Corredor Mat. XXXX │
└─────────────────────┘
```
### **Modelo 3: Etiqueta Tour Virtual (10x10cm)**
```
┌───────────────────────────┐
│ TOUR VIRTUAL 360° │
│ Visita este inmueble │
│ ¡sin salir del sofá! │
│ │
│ [CÓDIGO QR GRANDE] │
│ │
│ 📱 Apunta tu cámara │
│ │
│ Inmobiliaria Success │
│ (021) 123-4567 │
└───────────────────────────┘
```
---
## Mejores Prácticas de Impresión
### **Materiales Recomendados**
**Para carteles externos:**
- **Vinilo adhesivo resistente a UV**
- **Lona con impresión UV**
- **ACM (Aluminio Compuesto)**
Duración: 2-3 años expuestos al sol
**Para etiquetas adhesivas:**
- **Papel BOPP (Polipropileno)**
- **Vinilo blanco brillante**
- **Papel couché con laminación**
Duración: 6-12 meses
**Para volantes:**
- **Couché 115g o 150g**
- **Barniz localizado en el código QR** (destaque)
### **Pruebas Antes de Imprimir en Masa**
⚠️ **SIEMPRE haz esto:**
1. Imprime 1 muestra en tamaño real
2. Prueba con 5 celulares diferentes
3. Prueba a diferentes distancias
4. Prueba con poca luz
5. Solo entonces imprime grandes cantidades
**Celulares para probar:**
- iPhone (iOS actualizado)
- Samsung (Android)
- Xiaomi o Motorola (Android popular)
### **Dónde Imprimir**
**Imprentas rápidas en Asunción:**
- Etiquetas adhesivas: 100 unidades por Gs. 150.000-350.000
- Volantes A5: 1000 unidades por Gs. 800.000-1.500.000
**Online (más económico):**
- Mercado Libre Paraguay
- Imprentas locales con pedido online
**Consejo**: ¡Pide presupuesto en 3 lugares diferentes!
---
## Errores Comunes que Corredores Deben Evitar
### ❌ **Error 1: Código QR Muy Pequeño**
**Problema**: En carteles de calle vistos de lejos, QR pequeño no funciona
**Solución**: Mínimo 15x15cm para carteles externos
### ❌ **Error 2: Colores con Bajo Contraste**
**Problema**: QR amarillo en fondo blanco no escanea
**Solución**: Usa siempre colores oscuros en fondo claro (o viceversa)
### ❌ **Error 3: No Probar Antes de Imprimir**
**Problema**: Imprime 1000 volantes y descubre que QR no funciona
**Solución**: Siempre prueba impresión piloto
### ❌ **Error 4: URL Rota o Temporal**
**Problema**: Link del inmueble expira, QR queda inútil
**Solución**: Usa URLs permanentes o QR dinámico editable
### ❌ **Error 5: Sin Instrucción de Uso**
**Problema**: Persona mayor no sabe qué hacer con el QR
**Solución**: Agrega "Apunta la cámara del celular aquí"
### ❌ **Error 6: Código QR Único para Todos los Inmuebles**
**Problema**: No sabes qué inmueble generó el lead
**Solución**: Crea QR específico para cada propiedad
### ❌ **Error 7: Material de Baja Calidad**
**Problema**: Etiqueta se desvanece en 1 mes al sol
**Solución**: Invierte en material UV resistente
---
## Casos de Éxito en Paraguay
### **Caso 1: Corredor en Asunción**
**Estrategia**: Colocó código QR en todos los 12 carteles de inmuebles
**Resultado:**
- 98 escaneos en el primer mes
- 28 conversaciones en WhatsApp
- 7 visitas agendadas
- 2 ventas cerradas
**ROI**: Invirtió Gs. 500.000 en etiquetas, facturó comisiones millonarias
### **Caso 2: Inmobiliaria en Ciudad del Este**
**Estrategia**: Volantes con QR para tour virtual de lanzamiento
**Resultado:**
- 3.000 volantes distribuidos
- 645 accesos al tour virtual
- 112 registros de interesados
- 18 departamentos vendidos en preventa
### **Caso 3: Corredor Autónomo en Encarnación**
**Estrategia**: Etiquetas adhesivas en establecimientos asociados
**Resultado:**
- 150 etiquetas distribuidas en 30 locales
- 52 nuevos contactos en 3 meses
- 9 evaluaciones de inmuebles agendadas
- 2 captaciones exclusivas
---
## Preguntas Frecuentes
### **¿Necesito pagar para crear código QR?**
¡No! En QR Rapido creas códigos QR ilimitados gratuitamente. Solo pagas si quieres recursos premium como rastreo avanzado.
### **¿El código QR funciona para siempre?**
Códigos QR estáticos (gratuitos) funcionan para siempre, pero no pueden ser editados. Códigos QR dinámicos (pagos) pueden ser editados incluso después de impresos.
### **¿Qué tipo de QR usar en carteles de inmuebles?**
Recomiendo **WhatsApp** con mensaje pre-llenado. Así el cliente ya inicia la conversación sabiendo de qué inmueble se trata.
### **¿Puedo colocar logo de la inmobiliaria en el QR?**
Sí, ¡pero con cuidado! Logos muy grandes pueden dificultar la lectura. Mantén el logo pequeño (máximo 20% del QR).
### **¿Cuántas personas escanean QR en carteles?**
Según investigaciones, entre 5-15% de las personas que ven el cartel escanean el QR. En áreas movimentadas, esto puede generar decenas de leads por mes.
### **¿El código QR funciona de noche?**
Sí, siempre que haya alguna iluminación (poste de calle, luz de la pantalla del celular ya ayuda). Para mejor resultado, ilumina el cartel.
### **¿Puedo usar el mismo QR en varios materiales?**
Puedes, pero no es recomendado. Crea códigos QR diferentes para saber de dónde viene cada lead (cartel, volante, etiqueta, etc).
---
## Checklist del Corredor Profesional
Antes de imprimir, verifica:
- [ ] Código QR probado en al menos 3 celulares diferentes
- [ ] Tamaño adecuado para la distancia de escaneo
- [ ] Alto contraste entre QR y fondo
- [ ] Llamada a la acción clara ("Escanea aquí")
- [ ] Información de contacto visible (nombre, matrícula, teléfono)
- [ ] URL corta si es link (más fácil de escanear)
- [ ] Material de impresión resistente (especialmente externo)
- [ ] Margen de seguridad alrededor del QR (mínimo 1cm)
---
## Conclusión
Los códigos QR son la **herramienta más económica y eficaz** para corredores modernos captar leads calificados. Con inversión de menos de Gs. 500.000 en etiquetas y volantes, puedes:
✅ Captar leads 24 horas al día
✅ Facilitar el contacto instantáneo vía WhatsApp
✅ Mostrar tours virtuales impresionantes
✅ Rastrear qué materiales generan más resultado
✅ Destacarte de la competencia conservadora
**El mercado inmobiliario está cada vez más digital. Quien no se adapta, queda atrás.**
---
## ¡Empieza Ahora!
**Crea tu primer código QR para corredor gratuitamente:**
1. [Generar Código QR de WhatsApp](/) - Para carteles de inmuebles
2. [Generar Código QR vCard](/) - Para tarjetas de visita
3. [Generar Código QR de URL](/) - Para tour virtual
**¡Transforma tus carteles y volantes en máquinas de captar leads!** 🏠📱🚀

View File

@ -0,0 +1,598 @@
---
title: "Código QR para Corredores de Inmuebles: Guía Completa para Etiquetas y Volantes"
description: "Descubre cómo usar código QR en etiquetas adhesivas, carteles y volantes inmobiliarios. Aumenta tus ventas con tecnología gratuita y profesional."
keywords: "codigo qr corredor inmuebles, etiqueta corredor inmobiliaria, qr inmobiliaria, etiquetas adhesivas corredores, divulgar corredor propiedades, qr cartel se vende"
author: "QR Rapido"
date: 2025-10-10
lastmod: 2025-10-10
image: "/images/tutoriais/qr-code-corretor-imoveis-hero.jpg"
---
# Código QR para Corredores de Inmuebles: Guía Completa para Etiquetas y Volantes
Si eres corredor de inmuebles, sabes que **captar leads calificados** es esencial para cerrar negocios. Imagina transformar tus carteles de "Se Vende", volantes y etiquetas adhesivas en herramientas interactivas que conectan clientes directamente a tu WhatsApp, ficha del inmueble o tarjeta de visita digital - todo esto **gratuitamente** con códigos QR.
En esta guía completa, aprenderás a crear y aplicar códigos QR profesionales en materiales inmobiliarios, aumentando tus conversiones y destacándote de la competencia.
## ¿Por Qué los Corredores de Inmuebles Deben Usar Códigos QR?
### **Ventajas Comprobadas**
- ✅ **Captación 24/7**: Tu cartel trabaja para ti incluso cuando estás durmiendo
- ✅ **Contacto Instantáneo**: Cliente escanea y ya está en tu WhatsApp
- ✅ **Cero Escritura**: Elimina errores al anotar números
- ✅ **Rastreo**: Sabe cuántas personas se interesaron
- ✅ **Profesionalismo**: Demuestra modernidad e innovación
- ✅ **Costo Cero**: Genera códigos QR ilimitados gratuitamente
- ✅ **Tour Virtual**: Lleva al cliente dentro del inmueble virtualmente
### **Estadísticas del Mercado**
Según estudios del sector inmobiliario:
- **78%** de los compradores investigan propiedades por celular
- **65%** prefieren contacto vía WhatsApp en lugar de llamada
- **43%** escanean códigos QR en carteles de inmuebles cuando los ven
- Corredores que usan código QR tienen **35% más leads** mensuales
---
## Dónde Aplicar Códigos QR en Marketing Inmobiliario
### **1. Carteles de "Se Vende" y "Se Alquila"**
**¡El uso más poderoso!** El cartel al frente del inmueble es visto por cientos de personas diariamente.
**Qué colocar en el código QR:**
- Link directo a tu WhatsApp
- vCard con tus contactos completos
- Tour virtual 360° del inmueble
- Ficha técnica detallada (PDF)
- Video del inmueble en YouTube
**Consejo profesional**: Coloca texto llamativo como:
- "Escanea y agenda tu visita AHORA"
- "Tour Virtual - Apunta tu cámara aquí"
- "WhatsApp Directo del Corredor"
### **2. Etiquetas Adhesivas y Tags**
Etiquetas pequeñas (5x5cm hasta 10x10cm) son perfectas para:
**Aplicaciones:**
- Pegar en autos de la inmobiliaria
- Fijar en portones de inmuebles
- Aplicar en vitrinas de locales
- Distribuir en establecimientos asociados
- Colocar en ascensores de edificios
**Ventajas:**
- Bajo costo de impresión
- Fácil distribución masiva
- Pueden ser cambiadas rápidamente
- Óptimas para acciones promocionales
### **3. Volantes y Flyers**
¡Transforma volantes de papel en herramientas digitales!
**Dónde aplicar:**
- Esquina superior derecha (lugar de mayor atención)
- Centro, si es el foco principal
- Reverso, con llamada destacada
**Contenido recomendado:**
- Portafolio digital de inmuebles
- Formulario de registro
- Calculadora de financiamiento
- Lista completa de propiedades disponibles
### **4. Folders y Revistas Inmobiliarias**
Materiales impresos premium merecen códigos QR estratégicos.
**Uso ideal:**
- 1 QR por inmueble destacado
- QR en la portada para portafolio completo
- QR en la contraportada con tus contactos
- QR en cada página con más información
### **5. Tarjetas de Visita**
¡El clásico nunca pasa de moda, pero puede ser mejorado!
**Código QR en la tarjeta permite:**
- Guardar contacto automáticamente (vCard)
- Ver portafolio online
- Agendar reunión directo en la agenda
- Enviar mensaje vía WhatsApp
### **6. Firma de Email**
¡Cada email que envías es una oportunidad!
**Incluye código QR para:**
- Tu vCard completo
- Último lanzamiento inmobiliario
- Evaluación gratuita de inmueble
- Agendamiento de visitas
---
## Paso a Paso: Cómo Crear Código QR para Corredores
Voy a mostrar cómo crear **3 tipos de códigos QR** esenciales para corredores:
### **Tipo 1: Código QR de vCard (Tarjeta de Visita Digital)**
Perfecto para: Tarjetas de visita, firma de email, credenciales
**[INSERTAR IMAGEN 1 AQUÍ: Selección del tipo de código QR - vCard destacado]**
#### Paso 1: Selecciona "Tarjeta de Visita"
Accede al generador y elige la opción **Tarjeta de Visita** en el menú de tipos.
#### Paso 2: Completa tus Datos Profesionales
**Información obligatoria:**
- **Nombre completo**: Juan Silva
- **Cargo**: Corredor de Inmuebles - Matrícula 12345
- **Empresa**: Inmobiliaria Success
- **Teléfono**: +1 555 123-4567
- **Email**: juan.silva@inmuebles.com
- **Website**: www.juansilva.inmuebles.com
**Campos opcionales estratégicos:**
- WhatsApp Business
- Instagram profesional
- LinkedIn
- Canal de YouTube con tours virtuales
#### Paso 3: Genera y Descarga
Haz clic en **"Generar Código QR"** y descarga en alta resolución.
**Dónde usar este QR:**
- Etiquetas adhesivas en el auto
- Tarjetas de visita
- Firma de email
- Credencial profesional
---
### **Tipo 2: Código QR para WhatsApp Directo**
Perfecto para: Carteles de inmuebles, volantes, anuncios
**[INSERTAR IMAGEN 3 AQUÍ: Formulario de WhatsApp con código QR completado]**
#### Paso 1: Selecciona "WhatsApp"
En el generador, elige la opción **WhatsApp** (o SMS si prefieres).
#### Paso 2: Configura el Mensaje Pre-llenado
**Ejemplo de mensaje eficaz:**
```
¡Hola! Vi el cartel del inmueble en [CALLE/BARRIO] y me gustaría agendar una visita. ¿Puede pasarme más información?
```
**Por qué el mensaje pre-llenado funciona:**
- Cliente no necesita pensar qué escribir
- Ya sabes de qué inmueble está hablando
- Aumenta en 80% la tasa de conversión
#### Paso 3: Personaliza para Cada Inmueble
**Consejo importante**: ¡Crea códigos QR diferentes para cada inmueble!
Ejemplo para apartamento en Recoleta:
```
¡Hola! Vi el cartel del Departamento 3 dormitorios en Recoleta (Ref: DEPT-001). Me gustaría saber más detalles y agendar visita.
```
**Ventaja**: ¡Ya sabes exactamente qué inmueble quiere ver el cliente!
---
### **Tipo 3: Código QR para URL (Tour Virtual / Ficha Técnica)**
Perfecto para: Inmuebles de alto estándar, lanzamientos, propiedades rurales
**[INSERTAR IMAGEN 4 AQUÍ: Ejemplo de formulario URL completado]**
#### Paso 1: Prepara el Contenido Digital
Antes de crear el QR, necesitas tener:
**Opción A - Tour Virtual:**
- Video en YouTube del inmueble
- Tour 360° (Google Street View, Matterport)
- Galería de fotos en Instagram/Facebook
**Opción B - Landing Page:**
- Ficha técnica completa del inmueble
- Fotos en alta resolución
- Mapa de ubicación
- Calculadora de financiamiento
- Formulario de interés
#### Paso 2: Acorta la URL (¡Importante!)
Usa acortadores como:
- bit.ly
- tinyurl.com
- QR Rapido (si tiene función de acortamiento)
**Ejemplo:**
- ❌ URL larga: `https://www.miinmobiliaria.com/inmuebles/departamento-3-dormitorios-recoleta-ref-dept001?utm_source=cartel`
- ✅ URL corta: `bit.ly/dept-recoleta-001`
**Ventaja de URL corta**: Genera código QR más simple y fácil de escanear
#### Paso 3: Crea el Código QR
Pega la URL corta en el campo **URL/Link** y genera el código.
---
## Cómo Personalizar tu Código QR Profesionalmente
**[INSERTAR IMAGEN 2 AQUÍ: Panel de personalización con colores personalizados]**
### **Elección de Colores Estratégicos**
**Para Inmobiliarias Tradicionales:**
- Azul marino + Blanco (confianza, seriedad)
- Negro + Dorado (lujo, exclusividad)
- Verde oscuro + Blanco (crecimiento, estabilidad)
**Para Inmobiliarias Modernas:**
- Naranja + Blanco (energía, innovación)
- Púrpura + Blanco (creatividad, diferenciación)
- Rojo + Blanco (urgencia, acción)
**Regla de oro**: ¡Siempre mantén alto contraste entre color principal y fondo!
### **Tamaños Recomendados por Aplicación**
**Carteles de calle (distancia 2-5 metros):**
- Código QR: 15x15cm o mayor
- Resolución: 500px mínimo
**Volantes A5/A4:**
- Código QR: 4x4cm a 6x6cm
- Resolución: 300px
**Etiquetas adhesivas:**
- Código QR: 5x5cm (tamaño del adhesivo)
- Resolución: 300px
**Tarjetas de visita:**
- Código QR: 2,5x2,5cm
- Resolución: 200px
### **Agrega Llamadas a la Acción (CTA)**
¡Nunca coloques solo el código QR! Agrega texto atractivo:
**Ejemplos eficaces:**
- 📱 "Apunta la cámara y habla conmigo en WhatsApp"
- 🏠 "Tour Virtual 360° - Escanea Aquí"
- 💬 "Agenda tu Visita Ahora"
- 📋 "Ve Fotos y Detalles Completos"
- 🎯 "Guarda Mi Contacto Automáticamente"
---
## Estrategias Avanzadas para Corredores
### **1. Códigos QR Rastreables (Dinámicos)**
Usa servicios de código QR dinámico para:
- Saber cuántas personas escanearon
- Ver horario de los escaneos
- Identificar ubicación aproximada
- Cambiar el destino sin reimprimir
**Cuándo usar:**
- Campañas con muchos materiales impresos
- Pruebas A/B de diferentes enfoques
- Carteles permanentes en inmuebles
### **2. Múltiples Códigos QR en el Mismo Cartel**
¿Cartel grande? ¡Usa 2-3 códigos QR diferentes!
**Ejemplo de cartel completo:**
- **QR 1** (arriba): "Habla en WhatsApp"
- **QR 2** (centro): "Tour Virtual 360°"
- **QR 3** (abajo): "Guarda Mi Contacto"
**Ventaja**: Cliente elige la acción que prefiere hacer
### **3. Código QR + Realidad Aumentada**
Para lanzamientos y emprendimientos:
- Código QR lleva a app de RA
- Cliente apunta celular y ve el edificio terminado
- Visualiza departamento decorado
- ¡Extremadamente impactante!
### **4. Campañas de Temporada**
Crea códigos QR específicos para:
- **Enero**: "Planifica este año - Compra tu inmueble"
- **Junio**: "Vacaciones en tu Nuevo Hogar"
- **Noviembre**: "Black Friday Inmobiliaria"
- **Diciembre**: "Empieza el Año en Casa Propia"
### **5. Alianzas Estratégicas**
Distribuye etiquetas con código QR en:
- Tiendas de materiales de construcción
- Oficinas de arquitectura
- Gestorías y escribanías
- Gimnasios y restaurantes del barrio
- Edificios comerciales (tablero de avisos)
---
## Modelos de Etiquetas Adhesivas Profesionales
### **Modelo 1: Etiqueta Minimalista (5x5cm)**
```
┌─────────────────┐
│ [CÓDIGO QR] │
│ │
│ Juan Silva │
│ Matrícula 12345 │
│ (555) 123-4567 │
└─────────────────┘
```
### **Modelo 2: Etiqueta con Destaque (7x7cm)**
```
┌─────────────────────┐
│ VENDE TU INMUEBLE │
│ SIN BUROCRACIA │
│ │
│ [CÓDIGO QR] │
│ │
│ "Escanea y habla │
│ directo conmigo" │
│ │
│ Juan Silva │
│ Corredor Mat. XXXX │
└─────────────────────┘
```
### **Modelo 3: Etiqueta Tour Virtual (10x10cm)**
```
┌───────────────────────────┐
│ TOUR VIRTUAL 360° │
│ Visita este inmueble │
│ ¡sin salir del sofá! │
│ │
│ [CÓDIGO QR GRANDE] │
│ │
│ 📱 Apunta tu cámara │
│ │
│ Inmobiliaria Success │
│ (555) 123-4567 │
└───────────────────────────┘
```
---
## Mejores Prácticas de Impresión
### **Materiales Recomendados**
**Para carteles externos:**
- **Vinilo adhesivo resistente a UV**
- **Lona con impresión UV**
- **ACM (Aluminio Compuesto)**
Duración: 2-3 años expuestos al sol
**Para etiquetas adhesivas:**
- **Papel BOPP (Polipropileno)**
- **Vinilo blanco brillante**
- **Papel couché con laminación**
Duración: 6-12 meses
**Para volantes:**
- **Couché 115g o 150g**
- **Barniz localizado en el código QR** (destaque)
### **Pruebas Antes de Imprimir en Masa**
⚠️ **SIEMPRE haz esto:**
1. Imprime 1 muestra en tamaño real
2. Prueba con 5 celulares diferentes
3. Prueba a diferentes distancias
4. Prueba con poca luz
5. Solo entonces imprime grandes cantidades
**Celulares para probar:**
- iPhone (iOS actualizado)
- Samsung (Android)
- Xiaomi o Motorola (Android popular)
### **Dónde Imprimir**
**Imprentas rápidas locales:**
- Etiquetas adhesivas: consulta precios en tu zona
- Volantes A5: consulta precios en tu zona
**Online (más económico):**
- Marketplaces locales
- Imprentas con pedido online
**Consejo**: ¡Pide presupuesto en 3 lugares diferentes!
---
## Errores Comunes que Corredores Deben Evitar
### ❌ **Error 1: Código QR Muy Pequeño**
**Problema**: En carteles de calle vistos de lejos, QR pequeño no funciona
**Solución**: Mínimo 15x15cm para carteles externos
### ❌ **Error 2: Colores con Bajo Contraste**
**Problema**: QR amarillo en fondo blanco no escanea
**Solución**: Usa siempre colores oscuros en fondo claro (o viceversa)
### ❌ **Error 3: No Probar Antes de Imprimir**
**Problema**: Imprime 1000 volantes y descubre que QR no funciona
**Solución**: Siempre prueba impresión piloto
### ❌ **Error 4: URL Rota o Temporal**
**Problema**: Link del inmueble expira, QR queda inútil
**Solución**: Usa URLs permanentes o QR dinámico editable
### ❌ **Error 5: Sin Instrucción de Uso**
**Problema**: Persona mayor no sabe qué hacer con el QR
**Solución**: Agrega "Apunta la cámara del celular aquí"
### ❌ **Error 6: Código QR Único para Todos los Inmuebles**
**Problema**: No sabes qué inmueble generó el lead
**Solución**: Crea QR específico para cada propiedad
### ❌ **Error 7: Material de Baja Calidad**
**Problema**: Etiqueta se desvanece en 1 mes al sol
**Solución**: Invierte en material UV resistente
---
## Casos de Éxito
### **Caso 1: Corredor en zona urbana**
**Estrategia**: Colocó código QR en todos los 12 carteles de inmuebles
**Resultado:**
- 98 escaneos en el primer mes
- 28 conversaciones en WhatsApp
- 7 visitas agendadas
- 2 ventas cerradas
**ROI**: Invirtió en etiquetas, facturó comisiones significativas
### **Caso 2: Inmobiliaria en ciudad grande**
**Estrategia**: Volantes con QR para tour virtual de lanzamiento
**Resultado:**
- 3.000 volantes distribuidos
- 645 accesos al tour virtual
- 112 registros de interesados
- 18 departamentos vendidos en preventa
### **Caso 3: Corredor Autónomo**
**Estrategia**: Etiquetas adhesivas en establecimientos asociados
**Resultado:**
- 150 etiquetas distribuidas en 30 locales
- 52 nuevos contactos en 3 meses
- 9 evaluaciones de inmuebles agendadas
- 2 captaciones exclusivas
---
## Preguntas Frecuentes
### **¿Necesito pagar para crear código QR?**
¡No! En QR Rapido creas códigos QR ilimitados gratuitamente. Solo pagas si quieres recursos premium como rastreo avanzado.
### **¿El código QR funciona para siempre?**
Códigos QR estáticos (gratuitos) funcionan para siempre, pero no pueden ser editados. Códigos QR dinámicos (pagos) pueden ser editados incluso después de impresos.
### **¿Qué tipo de QR usar en carteles de inmuebles?**
Recomiendo **WhatsApp** con mensaje pre-llenado. Así el cliente ya inicia la conversación sabiendo de qué inmueble se trata.
### **¿Puedo colocar logo de la inmobiliaria en el QR?**
Sí, ¡pero con cuidado! Logos muy grandes pueden dificultar la lectura. Mantén el logo pequeño (máximo 20% del QR).
### **¿Cuántas personas escanean QR en carteles?**
Según investigaciones, entre 5-15% de las personas que ven el cartel escanean el QR. En áreas movimentadas, esto puede generar decenas de leads por mes.
### **¿El código QR funciona de noche?**
Sí, siempre que haya alguna iluminación (poste de calle, luz de la pantalla del celular ya ayuda). Para mejor resultado, ilumina el cartel.
### **¿Puedo usar el mismo QR en varios materiales?**
Puedes, pero no es recomendado. Crea códigos QR diferentes para saber de dónde viene cada lead (cartel, volante, etiqueta, etc).
---
## Checklist del Corredor Profesional
Antes de imprimir, verifica:
- [ ] Código QR probado en al menos 3 celulares diferentes
- [ ] Tamaño adecuado para la distancia de escaneo
- [ ] Alto contraste entre QR y fondo
- [ ] Llamada a la acción clara ("Escanea aquí")
- [ ] Información de contacto visible (nombre, matrícula, teléfono)
- [ ] URL corta si es link (más fácil de escanear)
- [ ] Material de impresión resistente (especialmente externo)
- [ ] Margen de seguridad alrededor del QR (mínimo 1cm)
---
## Conclusión
Los códigos QR son la **herramienta más económica y eficaz** para corredores modernos captar leads calificados. Con una pequeña inversión en etiquetas y volantes, puedes:
✅ Captar leads 24 horas al día
✅ Facilitar el contacto instantáneo vía WhatsApp
✅ Mostrar tours virtuales impresionantes
✅ Rastrear qué materiales generan más resultado
✅ Destacarte de la competencia conservadora
**El mercado inmobiliario está cada vez más digital. Quien no se adapta, queda atrás.**
---
## ¡Empieza Ahora!
**Crea tu primer código QR para corredor gratuitamente:**
1. [Generar Código QR de WhatsApp](/) - Para carteles de inmuebles
2. [Generar Código QR vCard](/) - Para tarjetas de visita
3. [Generar Código QR de URL](/) - Para tour virtual
**¡Transforma tus carteles y volantes en máquinas de captar leads!** 🏠📱🚀

View File

@ -0,0 +1,630 @@
---
title: "QR Code for Real Estate Agents: Complete Guide for Labels and Flyers"
description: "Discover how to use QR Codes on adhesive labels, signs and real estate flyers. Increase your sales with free and professional technology."
keywords: "qr code real estate agent, realtor label, real estate qr code, adhesive labels realtors, promote real estate agent, qr code for sale sign"
author: "QR Rapido"
date: 2025-10-10
lastmod: 2025-10-10
image: "/images/tutoriais/qr-code-corretor-imoveis-hero.jpg"
---
# QR Code for Real Estate Agents: Complete Guide for Labels and Flyers
If you are a real estate agent, you know that **capturing qualified leads** is essential to closing deals. Imagine turning your "For Sale" signs, flyers and adhesive labels into interactive tools that connect clients directly to your WhatsApp, property listing or digital business card — all of this **for free** with QR Codes!
In this complete guide, you will learn how to create and apply professional QR Codes on real estate materials, increasing your conversions and standing out from the competition.
## Why Real Estate Agents Should Use QR Codes?
### **Proven Advantages**
- ✅ **24/7 Lead Generation**: Your sign works for you even while you sleep
- ✅ **Instant Contact**: Client scans and is already on your WhatsApp
- ✅ **Zero Typing**: Eliminates errors when noting numbers
- ✅ **Tracking**: Know how many people were interested
- ✅ **Professionalism**: Demonstrates modernity and innovation
- ✅ **Zero Cost**: Generate unlimited QR Codes for free
- ✅ **Virtual Tour**: Take the client inside the property virtually
### **Market Statistics**
According to real estate industry studies:
- **78%** of buyers research properties on their phones
- **65%** prefer contact via WhatsApp instead of a call
- **43%** scan QR Codes on property signs when they see them
- Agents who use QR Codes have **35% more monthly leads**
---
## Where to Apply QR Codes in Real Estate Marketing
### **1. "For Sale" and "For Rent" Signs**
**The most powerful use!** The sign in front of the property is seen by hundreds of people daily.
**What to put in the QR Code:**
- Direct link to your WhatsApp
- vCard with your complete contacts
- 360° virtual tour of the property
- Detailed property spec sheet (PDF)
- Property video on YouTube
**Professional tip**: Add eye-catching text like:
- "Scan and schedule your visit NOW"
- "Virtual Tour - Point your camera here"
- "Direct WhatsApp to the Agent"
### **2. Adhesive Labels and Tags**
Small labels (2"x2" to 4"x4") are perfect for:
**Applications:**
- Sticking on agency cars
- Fixing on property gates
- Applying to store windows
- Distributing at partner businesses
- Placing in building elevators
**Advantages:**
- Low printing cost
- Easy mass distribution
- Can be replaced quickly
- Great for promotional actions
### **3. Flyers and Leaflets**
Transform paper flyers into digital tools!
**Where to apply:**
- Top right corner (area of greatest attention)
- Center, if it's the main focus
- Back, with highlighted call to action
**Recommended content:**
- Digital property portfolio
- Registration form
- Mortgage calculator
- Complete list of available properties
### **4. Brochures and Real Estate Magazines**
Premium printed materials deserve strategic QR Codes.
**Ideal use:**
- 1 QR per featured property
- QR on the cover for the complete portfolio
- QR on the back cover with your contacts
- QR on each page with more information
### **5. Business Cards**
The classic never goes out of style, but can be enhanced!
**QR Code on the card allows:**
- Save contact automatically (vCard)
- View online portfolio
- Schedule a meeting directly in the calendar
- Send message via WhatsApp
### **6. Email Signature**
Every email you send is an opportunity!
**Include QR Code for:**
- Your complete vCard
- Latest property launch
- Free property valuation
- Scheduling visits
---
## Step by Step: How to Create QR Codes for Agents
Here's how to create **3 essential QR Code types** for real estate agents:
### **Type 1: vCard QR Code (Digital Business Card)**
Perfect for: Business cards, email signature, badges
#### Step 1: Select "Business Card"
Access the generator and choose the **Business Card** option from the type menu.
#### Step 2: Fill in Your Professional Data
**Required information:**
- **Full name**: John Smith
- **Title**: Real Estate Agent License #12345
- **Company**: Success Real Estate
- **Phone**: +1 555 987-6543
- **Email**: john.smith@realestate.com
- **Website**: www.johnsmith.realestate
- **Address**: 1000 Main St - New York, NY
**Strategic optional fields:**
- WhatsApp Business
- Professional Instagram
- LinkedIn
- YouTube channel with virtual tours
#### Step 3: Generate and Download
Click **"Generate QR Code"** and download in high resolution.
**Where to use this QR:**
- Adhesive labels on the car
- Business cards
- Email signature
- Professional badge
---
### **Type 2: Direct WhatsApp QR Code**
Perfect for: Property signs, flyers, ads
#### Step 1: Select "WhatsApp"
In the generator, choose the **WhatsApp** option (or SMS if you prefer).
#### Step 2: Set Up the Pre-Written Message
**Example of an effective message:**
```
Hi! I saw the property sign at [STREET/NEIGHBORHOOD] and would like to schedule a visit. Can you send me more information?
```
**Why a pre-written message works:**
- Client doesn't need to think about what to write
- You already know which property they're talking about
- Increases conversion rate by 80%
#### Step 3: Customize for Each Property
**Important tip**: Create different QR Codes for each property!
Example for a 3-bedroom apartment in downtown:
```
Hi! I saw the sign for the 3-bedroom Apartment downtown (Ref: APT-001). I'd like to know more details and schedule a visit.
```
**Advantage**: You already know exactly which property the client wants to see!
---
### **Type 3: URL QR Code (Virtual Tour / Spec Sheet)**
Perfect for: High-end properties, launches, rural properties
#### Step 1: Prepare the Digital Content
Before creating the QR, you need to have:
**Option A - Virtual Tour:**
- YouTube video of the property
- 360° tour (Google Street View, Matterport)
- Photo gallery on Instagram/Facebook
**Option B - Landing Page:**
- Complete property spec sheet
- High-resolution photos
- Location map
- Mortgage calculator
- Interest registration form
#### Step 2: Shorten the URL (Important!)
Use URL shorteners like:
- bit.ly
- tinyurl.com
**Example:**
- ❌ Long URL: `https://www.myrealestate.com/properties/3-bedroom-downtown-ref-apt001?utm_source=sign`
- ✅ Short URL: `bit.ly/apt-downtown-001`
**Advantage of short URL**: Generates simpler QR Code that's easier to scan
#### Step 3: Create the QR Code
Paste the short URL into the **URL/Link** field and generate the code.
---
## How to Customize Your QR Code Professionally
### **Strategic Color Choices**
**For Traditional Real Estate Agencies:**
- Navy blue + White (trust, seriousness)
- Black + Gold (luxury, exclusivity)
- Dark green + White (growth, stability)
**For Modern Real Estate Agencies:**
- Orange + White (energy, innovation)
- Purple + White (creativity, differentiation)
- Red + White (urgency, action)
**Golden rule**: Always maintain high contrast between primary color and background!
### **Recommended Sizes by Application**
**Street signs (2-15 ft distance):**
- QR Code: 6"x6" or larger
- Resolution: 500px minimum
**Letter/A4 flyers:**
- QR Code: 1.5"x1.5" to 2.5"x2.5"
- Resolution: 300px
**Adhesive labels:**
- QR Code: 2"x2" (sticker size)
- Resolution: 300px
**Business cards:**
- QR Code: 1"x1"
- Resolution: 200px
### **Add Calls to Action (CTA)**
Never place a QR Code alone! Add attractive text:
**Effective examples:**
- 📱 "Point your camera and message me on WhatsApp"
- 🏠 "360° Virtual Tour - Scan Here"
- 💬 "Schedule Your Visit Now"
- 📋 "See Full Photos and Details"
- 🎯 "Save My Contact Automatically"
---
## Advanced Strategies for Agents
### **1. Trackable (Dynamic) QR Codes**
Use dynamic QR Code services to:
- Know how many people scanned
- See scan times
- Identify approximate location
- Change the destination without reprinting
**When to use:**
- Campaigns with many printed materials
- A/B testing different approaches
- Permanent signs on properties
### **2. Multiple QR Codes on the Same Sign**
Large sign? Use 2-3 different QR Codes!
**Example of a complete sign:**
- **QR 1** (top): "Message on WhatsApp"
- **QR 2** (center): "360° Virtual Tour"
- **QR 3** (bottom): "Save My Contact"
**Advantage**: Client chooses the action they prefer
### **3. QR Code + Augmented Reality**
For launches and developments:
- QR Code leads to AR app
- Client points phone and sees the finished building
- Views decorated apartment
- Extremely impactful!
### **4. Seasonal Campaigns**
Create specific QR Codes for:
- **January**: "Plan the New Year - Buy Your Property"
- **June**: "Summer Vacation in Your New Home"
- **November**: "Black Friday Real Estate Sale"
- **December**: "Start the Year in Your Own Home"
### **5. Strategic Partnerships**
Distribute labels with QR Code at:
- Building materials stores
- Architecture offices
- Notary offices
- Neighborhood gyms and restaurants
- Commercial buildings (bulletin boards)
---
## Professional Adhesive Label Templates
### **Template 1: Minimalist Label (2"x2")**
```
┌─────────────────┐
│ [QR CODE] │
│ │
│ John Smith │
│ License #12345
│ (555) 987-6543 │
└─────────────────┘
```
### **Template 2: Highlighted Label (3"x3")**
```
┌─────────────────────┐
│ SELL YOUR PROPERTY │
│ HASSLE-FREE │
│ │
│ [QR CODE] │
│ │
│ "Scan and talk │
│ directly to me" │
│ │
│ John Smith │
│ Licensed Agent │
└─────────────────────┘
```
### **Template 3: Virtual Tour Label (4"x4")**
```
┌───────────────────────────┐
│ 360° VIRTUAL TOUR │
│ Visit this property │
│ from your couch! │
│ │
│ [LARGE QR CODE] │
│ │
│ 📱 Point your camera │
│ │
│ Success Real Estate │
│ (555) 987-6543 │
└───────────────────────────┘
```
---
## Best Printing Practices
### **Recommended Materials**
**For outdoor signs:**
- **UV-resistant vinyl**
- **UV printed banner**
- **ACM (Aluminum Composite)**
Durability: 2-3 years in direct sunlight
**For adhesive labels:**
- **BOPP (Polypropylene) paper**
- **Glossy white vinyl**
- **Coated paper with lamination**
Durability: 6-12 months
**For flyers:**
- **Coated 80lb or 100lb stock**
- **Spot UV varnish on QR Code** (for emphasis)
### **Testing Before Mass Printing**
⚠️ **ALWAYS do this:**
1. Print 1 sample at actual size
2. Test with 5 different phones
3. Test at different distances
4. Test in low light
5. Only then print large quantities
**Phones to test:**
- iPhone (updated iOS)
- Samsung (Android)
- Other Android brands (popular models)
### **Where to Print**
**Quick print shops:**
- Adhesive labels: 100 units for $15-50
- A5 flyers: 1000 units for $50-150
**Online (cheaper):**
- Vistaprint
- Overnight Prints
- Sticker Mule
**Tip**: Get quotes from 3 different places!
---
## Common Mistakes Agents Should Avoid
### ❌ **Mistake 1: QR Code Too Small**
**Problem**: On street signs viewed from a distance, a small QR won't work
**Solution**: Minimum 6"x6" for outdoor signs
### ❌ **Mistake 2: Colors with Low Contrast**
**Problem**: Yellow QR on white background won't scan
**Solution**: Always use dark colors on light backgrounds (or vice versa)
### ❌ **Mistake 3: Not Testing Before Printing**
**Problem**: Print 1000 flyers and discover the QR doesn't work
**Solution**: Always test with a pilot print
### ❌ **Mistake 4: Broken or Temporary URL**
**Problem**: Property link expires, QR becomes useless
**Solution**: Use permanent URLs or editable dynamic QR
### ❌ **Mistake 5: No Usage Instructions**
**Problem**: Older person doesn't know what to do with the QR
**Solution**: Add "Point your phone camera here"
### ❌ **Mistake 6: Single QR Code for All Properties**
**Problem**: Don't know which property generated the lead
**Solution**: Create a specific QR for each property
### ❌ **Mistake 7: Low Quality Materials**
**Problem**: Label fades in 1 month in the sun
**Solution**: Invest in UV-resistant materials
---
## Success Stories
### **Case 1: Agent in New York**
**Strategy**: Placed QR Code on all 15 property signs
**Results:**
- 127 scans in the first month
- 34 WhatsApp conversations
- 8 visits scheduled
- 2 sales closed
**ROI**: Invested $150 in labels, earned $28,000 in commissions
### **Case 2: Real Estate Agency**
**Strategy**: Flyers with QR for virtual tour of new development
**Results:**
- 5,000 flyers distributed
- 890 virtual tour accesses
- 156 registered interested buyers
- 23 apartments sold in pre-sale
### **Case 3: Independent Agent**
**Strategy**: Adhesive labels at partner businesses
**Results:**
- 200 labels distributed at 40 locations
- 67 new contacts in 3 months
- 12 property valuations scheduled
- 3 exclusive listings captured
---
## Complementary Tools
### **Digital Content Creation**
- **Canva**: Create label and flyer layouts
- **Matterport**: 360° virtual tours
- **YouTube**: Host property videos
- **Google Drive**: Property spec sheet PDFs
### **Lead Management**
- **Bitly**: Shorten URLs and track clicks
- **Google Analytics**: Monitor traffic
- **WhatsApp Business**: Organize conversations
- **CRM tools**: Real estate CRM solutions
### **Online Printing**
- **Vistaprint**: Labels and flyers
- **Overnight Prints**: Signs and banners
- **Sticker Mule**: Premium stickers
---
## Frequently Asked Questions
### **Do I need to pay to create a QR Code?**
No! At QR Rapido you create unlimited QR Codes for free. You only pay if you want premium features like advanced tracking.
### **Does the QR Code work forever?**
Static QR Codes (free) work forever, but cannot be edited. Dynamic QR Codes (paid) can be edited even after printing.
### **Which QR type to use on property signs?**
I recommend **WhatsApp** with a pre-written message. That way the client already starts the conversation knowing which property they're asking about.
### **Can I put the agency logo on the QR?**
Yes, but carefully! Logos that are too large can make it harder to read. Keep the logo small (maximum 20% of the QR).
### **How many people scan QR Codes on signs?**
According to research, between 5-15% of people who see the sign scan the QR. In busy areas, this can generate dozens of leads per month.
### **Does the QR Code work at night?**
Yes, as long as there is some lighting. For best results, illuminate the sign.
### **Can I use the same QR on multiple materials?**
You can, but it's not recommended. Create different QR Codes to know where each lead came from (sign, flyer, label, etc.).
### **How do I know if the QR is working?**
Test immediately after creating! Use your own phone camera and at least 2 other people's phones to make sure.
---
## Professional Agent Checklist
Before printing, verify:
- [ ] QR Code tested on at least 3 different phones
- [ ] Appropriate size for scanning distance
- [ ] High contrast between QR and background
- [ ] Clear call to action ("Scan here")
- [ ] Visible contact information (name, license, phone)
- [ ] Short URL if using a link (easier to scan)
- [ ] Durable printing material (especially for outdoor use)
- [ ] Safety margin around the QR (minimum 0.4")
---
## Conclusion
QR Codes are the **cheapest and most effective tool** for modern agents to capture qualified leads. With an investment of less than $100 in labels and flyers, you can:
✅ Capture leads 24 hours a day
✅ Facilitate instant contact via WhatsApp
✅ Show impressive virtual tours
✅ Track which materials generate the most results
✅ Stand out from the conservative competition
**The real estate market is increasingly digital. Those who don't adapt get left behind.**
---
## Get Started Now!
**Create your first real estate agent QR Code for free:**
1. [Generate WhatsApp QR Code](/) - For property signs
2. [Generate vCard QR Code](/) - For business cards
3. [Generate URL QR Code](/) - For virtual tours
**Turn your signs and flyers into lead generation machines!** 🏠📱🚀
---
## Bonus Materials
- 📋 Pre-written WhatsApp message template
- 🎨 Editable label templates
- 📊 QR Code tracking spreadsheet by property
- 🎯 Professional printing checklist
**Want to stand out in the real estate market? Use QR Codes strategically and watch your results multiply!**

View File

@ -0,0 +1,640 @@
---
title: "QR Code para Corretores de Imóveis: Guia Completo para Etiquetas e Panfletos"
description: "Descubra como usar QR Code em etiquetas adesivas, placas e panfletos imobiliários. Aumente suas vendas com tecnologia gratuita e profissional."
keywords: "qr code corretor imoveis, etiqueta corretor imoveis, qrcode imobiliaria, etiquetas adesivas corretores, divulgar corretor imoveis, qr code placa vende-se"
author: "QR Rapido"
date: 2025-10-10
lastmod: 2025-10-10
image: "/images/tutoriais/qr-code-corretor-imoveis-hero.jpg"
---
# QR Code para Corretores de Imóveis: Guia Completo para Etiquetas e Panfletos
Se você é corretor de imóveis, sabe que **captar leads qualificados** é essencial para fechar negócios. Imagine transformar suas placas de "Vende-se", panfletos e etiquetas adesivas em ferramentas interativas que conectam clientes diretamente ao seu WhatsApp, ficha do imóvel ou cartão de visita digital - tudo isso **gratuitamente** com QR Codes!
Neste guia completo, você aprenderá a criar e aplicar QR Codes profissionais em materiais imobiliários, aumentando suas conversões e se destacando da concorrência.
## Por Que Corretores de Imóveis Devem Usar QR Codes?
### **Vantagens Comprovadas**
- ✅ **Captação 24/7**: Sua placa trabalha para você mesmo quando está dormindo
- ✅ **Contato Instantâneo**: Cliente escaneia e já está no seu WhatsApp
- ✅ **Zero Digitação**: Elimina erros ao anotar números
- ✅ **Rastreamento**: Saiba quantas pessoas se interessaram
- ✅ **Profissionalismo**: Demonstra modernidade e inovação
- ✅ **Custo Zero**: Gere QR Codes ilimitados gratuitamente
- ✅ **Tour Virtual**: Leve o cliente para dentro do imóvel virtualmente
### **Estatísticas do Mercado**
Segundo estudos do setor imobiliário:
- **78%** dos compradores pesquisam imóveis pelo celular
- **65%** preferem contato via WhatsApp ao invés de ligação
- **43%** escaneiam QR Codes em placas de imóveis quando veem
- Corretores que usam QR Code têm **35% mais leads** mensais
---
## Onde Aplicar QR Codes no Marketing Imobiliário
### **1. Placas de "Vende-se" e "Aluga-se"**
**O uso mais poderoso!** A placa na frente do imóvel é vista por centenas de pessoas diariamente.
**O que colocar no QR Code:**
- Link direto para seu WhatsApp
- vCard com seus contatos completos
- Tour virtual 360° do imóvel
- Ficha técnica detalhada (PDF)
- Vídeo do imóvel no YouTube
**Dica profissional**: Coloque texto chamativo como:
- "Escanei e agende sua visita AGORA"
- "Tour Virtual - Aponte a câmera aqui"
- "WhatsApp Direto do Corretor"
### **2. Etiquetas Adesivas e Tags**
Etiquetas pequenas (5x5cm até 10x10cm) são perfeitas para:
**Aplicações:**
- Colar em carros da imobiliária
- Fixar em portões de imóveis
- Aplicar em vitrines de lojas
- Distribuir em estabelecimentos parceiros
- Colocar em elevadores de prédios
**Vantagens:**
- Baixo custo de impressão
- Fácil distribuição em massa
- Podem ser trocadas rapidamente
- Ótimas para ações promocionais
### **3. Panfletos e Flyers**
Transforme panfletos de papel em ferramentas digitais!
**Onde aplicar:**
- Canto superior direito (local de maior atenção)
- Centro, se for o foco principal
- Verso, com chamada destacada
**Conteúdo recomendado:**
- Portfólio digital de imóveis
- Formulário de cadastro
- Calculadora de financiamento
- Lista completa de imóveis disponíveis
### **4. Folders e Revistas Imobiliárias**
Materiais impressos premium merecem QR Codes estratégicos.
**Uso ideal:**
- 1 QR por imóvel destacado
- QR na capa para portfólio completo
- QR na contracapa com seus contatos
- QR em cada página com mais informações
### **5. Cartões de Visita**
O clássico nunca sai de moda, mas pode ser turbinado!
**QR Code no cartão permite:**
- Salvar contato automaticamente (vCard)
- Ver portfólio online
- Agendar reunião direto na agenda
- Enviar mensagem via WhatsApp
### **6. Assinatura de Email**
Todo email que você envia é uma oportunidade!
**Inclua QR Code para:**
- Seu vCard completo
- Último lançamento imobiliário
- Avaliação gratuita de imóvel
- Agendamento de visitas
---
## Passo a Passo: Como Criar QR Code para Corretores
Vou mostrar como criar **3 tipos de QR Codes** essenciais para corretores:
### **Tipo 1: QR Code de vCard (Cartão de Visita Digital)**
Perfeito para: Cartões de visita, assinatura de email, crachás
**[INSERIR IMAGEM 1 AQUI: Seleção do tipo de QR Code - vCard destacado]**
#### Passo 1: Selecione "Cartão de Visita"
Acesse o gerador e escolha a opção **Cartão de Visita** no menu de tipos.
#### Passo 2: Preencha seus Dados Profissionais
**Informações obrigatórias:**
- **Nome completo**: João Silva
- **Cargo**: Corretor de Imóveis CRECI 12345-F
- **Empresa**: Imobiliária Success
- **Telefone**: +55 11 98765-4321
- **Email**: joao.silva@imoveis.com.br
- **Website**: www.joaosilva.imoveis.br
- **Endereço**: Av. Paulista, 1000 - São Paulo, SP
**Campos opcionais estratégicos:**
- WhatsApp Business
- Instagram profissional
- LinkedIn
- Canal do YouTube com tours virtuais
#### Passo 3: Gere e Baixe
Clique em **"Gerar QR Code"** e baixe em alta resolução.
**Onde usar este QR:**
- Etiquetas adesivas no carro
- Cartões de visita
- Assinatura de email
- Crachá profissional
---
### **Tipo 2: QR Code para WhatsApp Direto**
Perfeito para: Placas de imóveis, panfletos, anúncios
**[INSERIR IMAGEM 3 AQUI: Formulário de WhatsApp QR Code preenchido]**
#### Passo 1: Selecione "WhatsApp"
No gerador, escolha a opção **WhatsApp** (ou SMS se preferir).
#### Passo 2: Configure a Mensagem Pré-Pronta
**Exemplo de mensagem eficaz:**
```
Olá! Vi a placa do imóvel na [RUA/BAIRRO] e gostaria de agendar uma visita. Pode me passar mais informações?
```
**Por que mensagem pré-pronta funciona:**
- Cliente não precisa pensar no que escrever
- Você já sabe de qual imóvel ele está falando
- Aumenta em 80% a taxa de conversão
#### Passo 3: Personalize para Cada Imóvel
**Dica importante**: Crie QR Codes diferentes para cada imóvel!
Exemplo para apartamento no Jardins:
```
Olá! Vi a placa do Apartamento 3 quartos no Jardins (Ref: APT-001). Gostaria de saber mais detalhes e agendar visita.
```
**Vantagem**: Você já sabe exatamente qual imóvel o cliente quer ver!
---
### **Tipo 3: QR Code para URL (Tour Virtual / Ficha Técnica)**
Perfeito para: Imóveis de alto padrão, lançamentos, propriedades rurais
**[INSERIR IMAGEM 4 AQUI: Exemplo de formulário URL completado]**
#### Passo 1: Prepare o Conteúdo Digital
Antes de criar o QR, você precisa ter:
**Opção A - Tour Virtual:**
- Vídeo no YouTube do imóvel
- Tour 360° (Google Street View, Matterport)
- Galeria de fotos no Instagram/Facebook
**Opção B - Landing Page:**
- Ficha técnica completa do imóvel
- Fotos em alta resolução
- Mapa de localização
- Calculadora de financiamento
- Formulário de interesse
#### Passo 2: Encurte a URL (Importante!)
Use encurtadores como:
- bit.ly
- tinyurl.com
- QR Rapido (se tiver função de encurtamento)
**Exemplo:**
- ❌ URL longa: `https://www.minhaibiliaria.com.br/imoveis/apartamento-3-quartos-jardins-sao-paulo-ref-apt001?utm_source=placa`
- ✅ URL curta: `bit.ly/apt-jardins-001`
**Vantagem da URL curta**: Gera QR Code mais simples e fácil de escanear
#### Passo 3: Crie o QR Code
Cole a URL curta no campo **URL/Link** e gere o código.
---
## Como Personalizar seu QR Code Profissionalmente
**[INSERIR IMAGEM 2 AQUI: Painel de personalização com cores personalizadas]**
### **Escolha de Cores Estratégicas**
**Para Imobiliárias Tradicionais:**
- Azul marinho + Branco (confiança, seriedade)
- Preto + Dourado (luxo, exclusividade)
- Verde escuro + Branco (crescimento, estabilidade)
**Para Imobiliárias Modernas:**
- Laranja + Branco (energia, inovação)
- Roxo + Branco (criatividade, diferenciação)
- Vermelho + Branco (urgência, ação)
**Regra de ouro**: Sempre mantenha alto contraste entre cor principal e fundo!
### **Tamanhos Recomendados por Aplicação**
**Placas de rua (distância 2-5 metros):**
- QR Code: 15x15cm ou maior
- Resolução: 500px mínimo
**Panfletos A5/A4:**
- QR Code: 4x4cm a 6x6cm
- Resolução: 300px
**Etiquetas adesivas:**
- QR Code: 5x5cm (tamanho do adesivo)
- Resolução: 300px
**Cartões de visita:**
- QR Code: 2,5x2,5cm
- Resolução: 200px
### **Adicione Chamadas para Ação (CTA)**
Nunca coloque apenas o QR Code sozinho! Adicione texto atrativo:
**Exemplos eficazes:**
- 📱 "Aponte a câmera e fale comigo no WhatsApp"
- 🏠 "Tour Virtual 360° - Escaneie Aqui"
- 💬 "Agende sua Visita Agora"
- 📋 "Veja Fotos e Detalhes Completos"
- 🎯 "Salve Meu Contato Automaticamente"
---
## Estratégias Avançadas para Corretores
### **1. QR Codes Rastreáveis (Dinâmicos)**
Use serviços de QR Code dinâmico para:
- Saber quantas pessoas escanearam
- Ver horário dos escaneamentos
- Identificar localização aproximada
- Mudar o destino sem reimprimir
**Quando usar:**
- Campanhas com muitos materiais impressos
- Testes A/B de diferentes abordagens
- Placas permanentes em imóveis
### **2. Múltiplos QR Codes na Mesma Placa**
Placa grande? Use 2-3 QR Codes diferentes!
**Exemplo de placa completa:**
- **QR 1** (topo): "Fale no WhatsApp"
- **QR 2** (centro): "Tour Virtual 360°"
- **QR 3** (rodapé): "Salve Meu Contato"
**Vantagem**: Cliente escolhe a ação que prefere fazer
### **3. QR Code + Realidade Aumentada**
Para lançamentos e empreendimentos:
- QR Code leva para app de RA
- Cliente aponta celular e vê o prédio pronto
- Visualiza apartamento decorado
- Extremamente impactante!
### **4. Campanhas Sazonais**
Crie QR Codes específicos para:
- **Janeiro**: "Planeje 2025 - Compre seu imóvel"
- **Junho**: "Férias de Julho no seu Novo Lar"
- **Novembro**: "Black Friday Imobiliária"
- **Dezembro**: "Comece o Ano na Casa Própria"
### **5. Parcerias Estratégicas**
Distribua etiquetas com QR Code em:
- Lojas de materiais de construção
- Escritórios de arquitetura
- Despachantes e cartórios
- Academias e restaurantes do bairro
- Prédios comerciais (quadro de avisos)
---
## Modelos de Etiquetas Adesivas Profissionais
### **Modelo 1: Etiqueta Minimalista (5x5cm)**
```
┌─────────────────┐
│ [QR CODE] │
│ │
│ João Silva │
│ CRECI 12345-F │
│ (11) 98765-4321 │
└─────────────────┘
```
### **Modelo 2: Etiqueta com Destaque (7x7cm)**
```
┌─────────────────────┐
│ VENDA SEU IMÓVEL │
│ SEM BUROCRACIA │
│ │
│ [QR CODE] │
│ │
│ "Escaneie e fale │
│ direto comigo" │
│ │
│ João Silva │
│ Corretor CRECI │
└─────────────────────┘
```
### **Modelo 3: Etiqueta Tour Virtual (10x10cm)**
```
┌───────────────────────────┐
│ TOUR VIRTUAL 360° │
│ Visite este imóvel │
│ sem sair do sofá! │
│ │
│ [QR CODE GRANDE] │
│ │
│ 📱 Aponte sua câmera │
│ │
│ Imobiliária Success │
│ (11) 98765-4321 │
└───────────────────────────┘
```
---
## Melhores Práticas de Impressão
### **Materiais Recomendados**
**Para placas externas:**
- **Vinil adesivo resistente a UV**
- **Lona com impressão UV**
- **ACM (Alumínio Composto)**
Duração: 2-3 anos expostos ao sol
**Para etiquetas adesivas:**
- **Papel BOPP (Polipropileno)**
- **Vinil branco brilhante**
- **Papel couché com laminação**
Duração: 6-12 meses
**Para panfletos:**
- **Couché 115g ou 150g**
- **Verniz localizado no QR Code** (destaque)
### **Testes Antes de Imprimir em Massa**
⚠️ **SEMPRE faça isso:**
1. Imprima 1 amostra em tamanho real
2. Teste com 5 celulares diferentes
3. Teste em diferentes distâncias
4. Teste com pouca luz
5. Só então imprima grandes quantidades
**Celulares para testar:**
- iPhone (iOS atualizado)
- Samsung (Android)
- Xiaomi ou Motorola (Android popular)
### **Onde Imprimir**
**Gráficas rápidas:**
- Etiquetas adesivas: 100 unidades por R$ 30-80
- Panfletos A5: 1000 unidades por R$ 150-300
**Online (mais barato):**
- Printi.com.br
- Gráfica KWG
- Gráfica Atual
**Dica**: Peça orçamento em 3 lugares diferentes!
---
## Erros Comuns que Corretores Devem Evitar
### ❌ **Erro 1: QR Code Muito Pequeno**
**Problema**: Em placas de rua vistas de longe, QR pequeno não funciona
**Solução**: Mínimo 15x15cm para placas externas
### ❌ **Erro 2: Cores com Baixo Contraste**
**Problema**: QR amarelo em fundo branco não escaneia
**Solução**: Use sempre cores escuras em fundo claro (ou vice-versa)
### ❌ **Erro 3: Não Testar Antes de Imprimir**
**Problema**: Imprime 1000 panfletos e descobre que QR não funciona
**Solução**: Sempre teste impressão piloto
### ❌ **Erro 4: URL Quebrada ou Temporária**
**Problema**: Link do imóvel expira, QR fica inútil
**Solução**: Use URLs permanentes ou QR dinâmico editável
### ❌ **Erro 5: Sem Instrução de Uso**
**Problema**: Pessoa mais velha não sabe o que fazer com o QR
**Solução**: Adicione "Aponte a câmera do celular aqui"
### ❌ **Erro 6: QR Code Único para Todos Imóveis**
**Problema**: Não sabe qual imóvel gerou o lead
**Solução**: Crie QR específico para cada propriedade
### ❌ **Erro 7: Material de Baixa Qualidade**
**Problema**: Etiqueta desbota em 1 mês no sol
**Solução**: Invista em material UV resistente
---
## Cases de Sucesso
### **Case 1: Corretor em São Paulo**
**Estratégia**: Colocou QR Code em todas as 15 placas de imóveis
**Resultado:**
- 127 escaneamentos no primeiro mês
- 34 conversas no WhatsApp
- 8 visitas agendadas
- 2 vendas fechadas
**ROI**: Investiu R$ 150 em etiquetas, faturou R$ 28.000 em comissões
### **Case 2: Imobiliária no Rio de Janeiro**
**Estratégia**: Panfletos com QR para tour virtual de lançamento
**Resultado:**
- 5.000 panfletos distribuídos
- 890 acessos ao tour virtual
- 156 cadastros de interessados
- 23 apartamentos vendidos na pré-venda
### **Case 3: Corretor Autônomo no Interior**
**Estratégia**: Etiquetas adesivas em estabelecimentos parceiros
**Resultado:**
- 200 etiquetas distribuídas em 40 locais
- 67 novos contatos em 3 meses
- 12 avaliações de imóveis agendadas
- 3 captações exclusivas
---
## Ferramentas Complementares
### **Criação de Conteúdo Digital**
- **Canva**: Criar layouts de etiquetas e panfletos
- **Matterport**: Tours virtuais 360°
- **YouTube**: Hospedar vídeos de imóveis
- **Google Drive**: PDFs de fichas técnicas
### **Gestão de Leads**
- **Bitly**: Encurtar URLs e rastrear cliques
- **Google Analytics**: Monitorar acessos
- **WhatsApp Business**: Organizar conversas
- **RD Station**: CRM imobiliário
### **Impressão Online**
- **Printi**: Etiquetas e panfletos
- **Gráfica KWG**: Placas e banners
- **Sticker Mule**: Adesivos premium
---
## Perguntas Frequentes
### **Preciso pagar para criar QR Code?**
Não! No QR Rapido você cria QR Codes ilimitados gratuitamente. Só paga se quiser recursos premium como rastreamento avançado.
### **O QR Code funciona para sempre?**
QR Codes estáticos (gratuitos) funcionam para sempre, mas não podem ser editados. QR Codes dinâmicos (pagos) podem ser editados mesmo depois de impressos.
### **Qual tipo de QR usar em placas de imóveis?**
Recomendo **WhatsApp** com mensagem pré-pronta. Assim o cliente já inicia a conversa sabendo de qual imóvel se trata.
### **Posso colocar logo da imobiliária no QR?**
Sim, mas com cuidado! Logos muito grandes podem dificultar a leitura. Mantenha a logo pequena (máximo 20% do QR).
### **Quantas pessoas escaneiam QR em placas?**
Segundo pesquisas, entre 5-15% das pessoas que veem a placa escaneiam o QR. Em áreas movimentadas, isso pode gerar dezenas de leads por mês.
### **QR Code funciona à noite?**
Sim, desde que haja alguma iluminação (poste de rua, luz da tela do celular já ajuda). Para melhor resultado, ilumine a placa.
### **Posso usar o mesmo QR em vários materiais?**
Pode, mas não é recomendado. Crie QR Codes diferentes para saber de onde vem cada lead (placa, panfleto, etiqueta, etc).
### **Como sei se o QR está funcionando?**
Teste imediatamente após criar! Use a câmera do seu celular e de pelo menos 2 amigos/familiares para garantir.
---
## Checklist do Corretor Profissional
Antes de imprimir, confira:
- [ ] QR Code testado em pelo menos 3 celulares diferentes
- [ ] Tamanho adequado para a distância de escaneamento
- [ ] Alto contraste entre QR e fundo
- [ ] Chamada para ação clara ("Escaneie aqui")
- [ ] Informações de contato visíveis (nome, CRECI, telefone)
- [ ] URL curta se for link (mais fácil de escanear)
- [ ] Material de impressão resistente (especialmente externo)
- [ ] Margem de segurança ao redor do QR (mínimo 1cm)
---
## Conclusão
QR Codes são a **ferramenta mais barata e eficaz** para corretores modernos captarem leads qualificados. Com investimento de menos de R$ 100 em etiquetas e panfletos, você pode:
✅ Captar leads 24 horas por dia
✅ Facilitar o contato instantâneo via WhatsApp
✅ Mostrar tours virtuais impressionantes
✅ Rastrear quais materiais geram mais resultado
✅ Se destacar da concorrência conservadora
**O mercado imobiliário está cada vez mais digital. Quem não se adapta, fica para trás.**
---
## Comece Agora!
**Crie seu primeiro QR Code para corretor gratuitamente:**
1. [Gerar QR Code de WhatsApp](/) - Para placas de imóveis
2. [Gerar QR Code vCard](/) - Para cartões de visita
3. [Gerar QR Code de URL](/) - Para tour virtual
**Transforme suas placas e panfletos em máquinas de captar leads!** 🏠📱🚀
---
## Materiais Bônus para Download
- 📋 Template de mensagem pré-pronta para WhatsApp
- 🎨 Modelos de etiquetas editáveis (Canva)
- 📊 Planilha de controle de QR Codes por imóvel
- 🎯 Checklist de impressão profissional
**Quer se destacar no mercado imobiliário? Use QR Codes de forma estratégica e veja seus resultados multiplicarem!**

View File

@ -1,243 +1,335 @@
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authentication.Google;
using Microsoft.AspNetCore.Authentication.MicrosoftAccount;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using QRRapidoApp.Models.ViewModels;
using QRRapidoApp.Services;
using System.Security.Claims;
namespace QRRapidoApp.Controllers
{
public class AccountController : Controller
{
private readonly IUserService _userService;
private readonly AdDisplayService _adDisplayService;
private readonly ILogger<AccountController> _logger;
private readonly IConfiguration _configuration;
public AccountController(IUserService userService, AdDisplayService adDisplayService,
ILogger<AccountController> logger, IConfiguration configuration)
{
_userService = userService;
_adDisplayService = adDisplayService;
_logger = logger;
_configuration = configuration;
}
[HttpGet]
public IActionResult Login(string returnUrl = "/")
{
ViewBag.ReturnUrl = returnUrl;
return View();
}
[HttpGet]
public IActionResult LoginGoogle(string returnUrl = "/")
{
var baseUrl = _configuration.GetSection("App:BaseUrl").Value;
var properties = new AuthenticationProperties
{
RedirectUri = $"{baseUrl}{Url.Action("GoogleCallback")}",
Items = { { "returnUrl", returnUrl } }
};
return Challenge(properties, GoogleDefaults.AuthenticationScheme);
}
[HttpGet]
public IActionResult LoginMicrosoft(string returnUrl = "/")
{
var redirectUrl = Url.Action("MicrosoftCallback", "Account", new { returnUrl });
var properties = new AuthenticationProperties { RedirectUri = redirectUrl };
return Challenge(properties, MicrosoftAccountDefaults.AuthenticationScheme);
}
[HttpGet]
public async Task<IActionResult> GoogleCallback()
{
return await HandleExternalLoginCallbackAsync(GoogleDefaults.AuthenticationScheme);
}
[HttpGet]
public async Task<IActionResult> MicrosoftCallback()
{
return await HandleExternalLoginCallbackAsync(MicrosoftAccountDefaults.AuthenticationScheme);
}
private async Task<IActionResult> HandleExternalLoginCallbackAsync(string scheme)
{
try
{
var result = await HttpContext.AuthenticateAsync(scheme);
if (!result.Succeeded)
{
_logger.LogWarning($"External authentication failed for scheme {scheme}");
return RedirectToAction("Login");
}
var email = result.Principal?.FindFirst(ClaimTypes.Email)?.Value;
var name = result.Principal?.FindFirst(ClaimTypes.Name)?.Value;
var providerId = result.Principal?.FindFirst(ClaimTypes.NameIdentifier)?.Value;
if (string.IsNullOrEmpty(email) || string.IsNullOrEmpty(providerId))
{
_logger.LogWarning($"Missing required claims from {scheme} authentication");
return RedirectToAction("Login");
}
// Find or create user
var user = await _userService.GetUserByProviderAsync(scheme, providerId);
if (user == null)
{
user = await _userService.CreateUserAsync(email, name ?? email, scheme, providerId);
}
else
{
await _userService.UpdateLastLoginAsync(user.Id);
}
// Create application claims
var claims = new List<Claim>
{
new Claim(ClaimTypes.NameIdentifier, user.Id),
new Claim(ClaimTypes.Email, user.Email),
new Claim(ClaimTypes.Name, user.Name),
new Claim("Provider", user.Provider),
new Claim("IsPremium", user.IsPremium.ToString())
};
var claimsIdentity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme);
var authProperties = new AuthenticationProperties
{
IsPersistent = true,
ExpiresUtc = DateTimeOffset.UtcNow.AddDays(30)
};
await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme,
new ClaimsPrincipal(claimsIdentity), authProperties);
var returnUrl = result.Properties?.Items != null && result.Properties.Items.ContainsKey("returnUrl") ? result.Properties?.Items["returnUrl"] : "/";
return RedirectToAction("Index", "Home");
}
catch (Exception ex)
{
_logger.LogError(ex, $"Error in external login callback for {scheme}");
return RedirectToAction("Login");
}
}
[HttpPost]
[Authorize]
public async Task<IActionResult> Logout()
{
await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
return RedirectToAction("Index", "Home");
}
[HttpGet]
[Authorize]
public async Task<IActionResult> Profile()
{
var userId = User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
if (string.IsNullOrEmpty(userId))
{
return RedirectToAction("Login");
}
var user = await _userService.GetUserAsync(userId);
if (user == null)
{
return RedirectToAction("Login");
}
ViewBag.QRHistory = await _userService.GetUserQRHistoryAsync(userId, 10);
ViewBag.MonthlyQRCount = await _userService.GetQRCountThisMonthAsync(userId);
ViewBag.IsPremium = await _adDisplayService.HasValidPremiumSubscription(userId);
return View(user);
}
[HttpGet]
public async Task<IActionResult> AdFreeStatus()
{
var userId = User?.FindFirst(ClaimTypes.NameIdentifier)?.Value;
if (string.IsNullOrEmpty(userId))
{
return Json(new AdFreeStatusViewModel
{
IsAdFree = false,
TimeRemaining = 0,
IsPremium = false
});
}
var shouldShowAds = await _adDisplayService.ShouldShowAds(userId);
var isPremium = await _adDisplayService.HasValidPremiumSubscription(userId);
var expiryDate = await _adDisplayService.GetAdFreeExpiryDate(userId);
var status = await _adDisplayService.GetAdFreeStatusAsync(userId);
return Json(new AdFreeStatusViewModel
{
IsAdFree = !shouldShowAds,
TimeRemaining = isPremium ? int.MaxValue : 0,
IsPremium = isPremium,
ExpiryDate = expiryDate,
SessionType = status
});
}
[HttpPost]
[Authorize]
public async Task<IActionResult> ExtendAdFreeTime(int minutes)
{
var userId = User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
// Método removido - sem extensão de tempo ad-free
return Json(new { success = false, message = "Feature not available" });
}
[HttpGet]
[Authorize]
public async Task<IActionResult> History()
{
var userId = User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
if (string.IsNullOrEmpty(userId))
{
return RedirectToAction("Login");
}
var history = await _userService.GetUserQRHistoryAsync(userId, 50);
return View(history);
}
[HttpPost]
[Authorize]
public async Task<IActionResult> UpdatePreferences(string language)
{
var userId = User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
if (string.IsNullOrEmpty(userId))
{
return Json(new { success = false });
}
try
{
var user = await _userService.GetUserAsync(userId);
if (user != null)
{
user.PreferredLanguage = language;
await _userService.UpdateUserAsync(user);
return Json(new { success = true });
}
}
catch (Exception ex)
{
_logger.LogError(ex, $"Error updating preferences for user {userId}");
}
return Json(new { success = false });
}
}
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authentication.Google;
using Microsoft.AspNetCore.Authentication.MicrosoftAccount;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using QRRapidoApp.Models.ViewModels;
using QRRapidoApp.Services;
using System.Security.Claims;
using System.Text.Json;
using System.Text;
using Microsoft.AspNetCore.DataProtection;
namespace QRRapidoApp.Controllers
{
public class AccountController : Controller
{
private readonly IUserService _userService;
private readonly AdDisplayService _adDisplayService;
private readonly ILogger<AccountController> _logger;
private readonly IConfiguration _configuration;
private readonly IDataProtector _protector;
public AccountController(IUserService userService, AdDisplayService adDisplayService,
ILogger<AccountController> logger, IConfiguration configuration,
IDataProtectionProvider dataProtection)
{
_userService = userService;
_adDisplayService = adDisplayService;
_logger = logger;
_configuration = configuration;
_protector = dataProtection.CreateProtector("OAuth.StateProtection");
}
[HttpGet]
public IActionResult Login(string returnUrl = "/")
{
_adDisplayService.SetViewBagAds(ViewBag);
ViewBag.ReturnUrl = returnUrl;
return View();
}
[HttpGet]
public IActionResult LoginGoogle(string returnUrl = "/")
{
var baseUrl = _configuration.GetSection("App:BaseUrl").Value;
// Criar state com dados criptografados em vez de sessão
var stateData = new OAuthStateData
{
ReturnUrl = returnUrl,
Nonce = Guid.NewGuid().ToString(),
Timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds()
};
var stateJson = JsonSerializer.Serialize(stateData);
var protectedState = _protector.Protect(stateJson);
var encodedState = Convert.ToBase64String(Encoding.UTF8.GetBytes(protectedState));
var properties = new AuthenticationProperties
{
RedirectUri = $"{baseUrl}{Url.Action("GoogleCallback")}",
Items = { { "state", encodedState } }
};
return Challenge(properties, GoogleDefaults.AuthenticationScheme);
}
[HttpGet]
public IActionResult LoginMicrosoft(string returnUrl = "/")
{
var baseUrl = _configuration.GetSection("App:BaseUrl").Value;
// Mesmo processo para Microsoft
var stateData = new OAuthStateData
{
ReturnUrl = returnUrl,
Nonce = Guid.NewGuid().ToString(),
Timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds()
};
var stateJson = JsonSerializer.Serialize(stateData);
var protectedState = _protector.Protect(stateJson);
var encodedState = Convert.ToBase64String(Encoding.UTF8.GetBytes(protectedState));
var redirectUrl = returnUrl == "/"
? $"{baseUrl}/Account/MicrosoftCallback"
: $"{baseUrl}/Account/MicrosoftCallback";
var properties = new AuthenticationProperties
{
RedirectUri = redirectUrl,
Items = { { "state", encodedState } }
};
return Challenge(properties, MicrosoftAccountDefaults.AuthenticationScheme);
}
[HttpGet]
public async Task<IActionResult> GoogleCallback(string state = null)
{
var returnUrl = await HandleExternalLoginCallbackAsync(GoogleDefaults.AuthenticationScheme, state);
return Redirect(returnUrl ?? "/");
}
[HttpGet]
public async Task<IActionResult> MicrosoftCallback(string state = null)
{
var returnUrl = await HandleExternalLoginCallbackAsync(MicrosoftAccountDefaults.AuthenticationScheme, state);
return Redirect(returnUrl ?? "/");
}
private async Task<string> HandleExternalLoginCallbackAsync(string scheme, string state = null)
{
try
{
_adDisplayService.SetViewBagAds(ViewBag);
// Recuperar returnUrl do state em vez da sessão
string returnUrl = "/";
if (!string.IsNullOrEmpty(state))
{
try
{
var decodedState = Encoding.UTF8.GetString(Convert.FromBase64String(state));
var unprotectedState = _protector.Unprotect(decodedState);
var stateData = JsonSerializer.Deserialize<OAuthStateData>(unprotectedState);
// Validar timestamp (não mais que 10 minutos)
var maxAge = DateTimeOffset.UtcNow.ToUnixTimeSeconds() - (10 * 60);
if (stateData.Timestamp > maxAge)
{
returnUrl = stateData.ReturnUrl ?? "/";
// Prevent redirect loop to login page
if (returnUrl.Contains("/Account/Login", StringComparison.OrdinalIgnoreCase))
{
returnUrl = "/";
}
}
else
{
_logger.LogWarning($"OAuth state expired for scheme {scheme}");
}
}
catch (Exception ex)
{
_logger.LogWarning(ex, $"Failed to decode OAuth state for scheme {scheme}");
}
}
var result = await HttpContext.AuthenticateAsync(scheme);
if (!result.Succeeded)
{
_logger.LogWarning($"External authentication failed for scheme {scheme}");
return "/Account/Login";
}
var email = result.Principal?.FindFirst(ClaimTypes.Email)?.Value;
var name = result.Principal?.FindFirst(ClaimTypes.Name)?.Value;
var providerId = result.Principal?.FindFirst(ClaimTypes.NameIdentifier)?.Value;
if (string.IsNullOrEmpty(email) || string.IsNullOrEmpty(providerId))
{
_logger.LogWarning($"Missing required claims from {scheme} authentication");
return "/Account/Login";
}
// Find or create user
var user = await _userService.GetUserByProviderAsync(scheme, providerId);
if (user == null)
{
// Fix CS8625: Ensure name is not null
var safeName = !string.IsNullOrEmpty(name) ? name : (email ?? "User");
user = await _userService.CreateUserAsync(email, safeName, scheme, providerId);
}
else
{
await _userService.UpdateLastLoginAsync(user.Id);
}
// Create application claims
var claims = new List<Claim>
{
new Claim(ClaimTypes.NameIdentifier, user.Id ?? string.Empty), // Fix CS8625
new Claim(ClaimTypes.Email, user.Email ?? string.Empty), // Fix CS8625
new Claim(ClaimTypes.Name, user.Name ?? string.Empty), // Fix CS8625
new Claim("Provider", user.Provider ?? string.Empty),
new Claim("IsPremium", user.IsPremium.ToString())
};
var claimsIdentity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme);
var authProperties = new AuthenticationProperties
{
IsPersistent = true,
ExpiresUtc = DateTimeOffset.UtcNow.AddDays(30)
};
await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme,
new ClaimsPrincipal(claimsIdentity), authProperties);
return returnUrl;
}
catch (Exception ex)
{
_logger.LogError(ex, $"Error in external login callback for {scheme}");
return "/Account/Login";
}
}
[HttpPost]
[Authorize]
public async Task<IActionResult> Logout()
{
await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
return RedirectToAction("Index", "Home");
}
[HttpGet]
[Authorize]
public async Task<IActionResult> Profile()
{
var userId = User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
if (string.IsNullOrEmpty(userId))
{
return RedirectToAction("Login");
}
var user = await _userService.GetUserAsync(userId);
if (user == null)
{
return RedirectToAction("Login");
}
// Ensure we are passing a non-null userId
ViewBag.QRHistory = await _userService.GetUserQRHistoryAsync(userId, 10);
ViewBag.MonthlyQRCount = await _userService.GetQRCountThisMonthAsync(userId);
ViewBag.IsPremium = await _adDisplayService.HasValidPremiumSubscription(userId);
_adDisplayService.SetViewBagAds(ViewBag);
return View(user);
}
[HttpGet]
public async Task<IActionResult> AdFreeStatus()
{
var userId = User?.FindFirst(ClaimTypes.NameIdentifier)?.Value;
if (string.IsNullOrEmpty(userId))
{
return Json(new AdFreeStatusViewModel
{
IsAdFree = false,
TimeRemaining = 0,
IsPremium = false
});
}
var shouldShowAds = await _adDisplayService.ShouldShowAds(userId);
var isPremium = await _adDisplayService.HasValidPremiumSubscription(userId);
var expiryDate = await _adDisplayService.GetAdFreeExpiryDate(userId);
var status = await _adDisplayService.GetAdFreeStatusAsync(userId);
return Json(new AdFreeStatusViewModel
{
IsAdFree = !shouldShowAds,
TimeRemaining = isPremium ? int.MaxValue : 0,
IsPremium = isPremium,
ExpiryDate = expiryDate,
SessionType = status
});
}
[HttpPost]
[Authorize]
public async Task<IActionResult> ExtendAdFreeTime(int minutes)
{
var userId = User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
// Método removido - sem extensão de tempo ad-free
return Json(new { success = false, message = "Feature not available" });
}
[HttpGet]
[Authorize]
public async Task<IActionResult> History()
{
_adDisplayService.SetViewBagAds(ViewBag);
var userId = User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
if (string.IsNullOrEmpty(userId))
{
return RedirectToAction("Login");
}
var history = await _userService.GetUserQRHistoryAsync(userId, 50);
return View(history);
}
[HttpPost]
[Authorize]
public async Task<IActionResult> UpdatePreferences(string language)
{
var userId = User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
if (string.IsNullOrEmpty(userId))
{
return Json(new { success = false });
}
try
{
var user = await _userService.GetUserAsync(userId);
if (user != null)
{
user.PreferredLanguage = language;
await _userService.UpdateUserAsync(user);
return Json(new { success = true });
}
}
catch (Exception ex)
{
_logger.LogError(ex, $"Error updating preferences for user {userId}");
}
return Json(new { success = false });
}
}
// Classe para dados do state
public class OAuthStateData
{
public string ReturnUrl { get; set; } = "/";
public string Nonce { get; set; } = "";
public long Timestamp { get; set; }
}
}

View File

@ -0,0 +1,212 @@
using Microsoft.AspNetCore.Mvc;
using QRRapidoApp.Data;
using QRRapidoApp.Models;
using QRRapidoApp.Services;
using MongoDB.Driver;
using System.Security.Claims;
namespace QRRapidoApp.Controllers
{
public class AdminController : Controller
{
private readonly MongoDbContext _context;
private readonly ILogger<AdminController> _logger;
private readonly IConfiguration _config;
private readonly IUserService _userService;
public AdminController(MongoDbContext context, ILogger<AdminController> logger, IConfiguration config, IUserService userService)
{
_context = context;
_logger = logger;
_config = config;
_userService = userService;
}
private bool IsAdmin()
{
// 1. Check if authenticated
if (User?.Identity?.IsAuthenticated != true) return false;
// 2. Get User Email
var userEmail = User.FindFirst(ClaimTypes.Email)?.Value;
if (string.IsNullOrEmpty(userEmail))
{
// Fallback: try to find user by ID to get email
var userId = User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
if (!string.IsNullOrEmpty(userId))
{
var user = _userService.GetUserAsync(userId).Result;
userEmail = user?.Email;
}
}
if (string.IsNullOrEmpty(userEmail)) return false;
// 3. Check against AllowedEmails list
var allowedEmails = _config.GetSection("Admin:AllowedEmails").Get<List<string>>() ?? new List<string>();
return allowedEmails.Contains(userEmail, StringComparer.OrdinalIgnoreCase);
}
private bool IsLocalhost()
{
var remoteIp = HttpContext.Connection.RemoteIpAddress;
return remoteIp != null &&
(remoteIp.ToString() == "127.0.0.1" ||
remoteIp.ToString() == "::1" ||
remoteIp.ToString() == "localhost");
}
// --- View Actions ---
[HttpGet("/Admin")]
public async Task<IActionResult> Index()
{
if (!IsAdmin()) return RedirectToAction("Index", "Home");
try
{
var orders = await _context.Orders
.Find(o => o.Status == "Pending")
.SortByDescending(o => o.CreatedAt)
.ToListAsync();
return View(orders);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error fetching pending orders for view");
return View(new List<Order>());
}
}
// --- API Endpoints ---
[HttpGet("api/Admin/Orders/Pending")]
public async Task<IActionResult> GetPendingOrders()
{
if (!IsAdmin()) return Unauthorized("Access denied. Admin rights required.");
try
{
var orders = await _context.Orders
.Find(o => o.Status == "Pending")
.SortByDescending(o => o.CreatedAt)
.ToListAsync();
return Ok(orders);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error fetching pending orders");
return StatusCode(500, new { error = "Internal server error" });
}
}
[HttpPost("api/Admin/Orders/{orderId}/Approve")]
public async Task<IActionResult> ApproveOrder(string orderId)
{
if (!IsAdmin()) return Unauthorized("Access denied. Admin rights required.");
try
{
var adminEmail = User.FindFirst(ClaimTypes.Email)?.Value ?? "unknown_admin";
// 1. Get the order
var order = await _context.Orders
.Find(o => o.Id == orderId)
.FirstOrDefaultAsync();
if (order == null) return NotFound("Order not found");
if (order.Status == "Paid") return BadRequest("Order already paid");
// 2. Update Order Status
var updateOrder = Builders<Order>.Update
.Set(o => o.Status, "Paid")
.Set(o => o.PaidAt, DateTime.UtcNow)
.Set(o => o.ApprovedBy, adminEmail);
await _context.Orders.UpdateOneAsync(o => o.Id == orderId, updateOrder);
// 3. Add Credits to User
var userUpdate = Builders<User>.Update.Inc(u => u.Credits, order.CreditsAmount);
await _context.Users.UpdateOneAsync(u => u.Id == order.UserId, userUpdate);
_logger.LogInformation($"Order {orderId} approved by {adminEmail}. {order.CreditsAmount} credits added to user {order.UserId}");
return Ok(new { success = true, message = "Order approved and credits added." });
}
catch (Exception ex)
{
_logger.LogError(ex, $"Error approving order {orderId}");
return StatusCode(500, new { error = "Internal server error" });
}
}
[HttpPost("api/Admin/Orders/{orderId}/Reject")]
public async Task<IActionResult> RejectOrder(string orderId)
{
if (!IsAdmin()) return Unauthorized("Access denied. Admin rights required.");
try
{
var adminEmail = User.FindFirst(ClaimTypes.Email)?.Value ?? "unknown_admin";
var update = Builders<Order>.Update
.Set(o => o.Status, "Rejected")
.Set(o => o.ApprovedBy, adminEmail); // Rejected by...
var result = await _context.Orders.UpdateOneAsync(o => o.Id == orderId, update);
if (result.ModifiedCount == 0) return NotFound("Order not found");
_logger.LogInformation($"Order {orderId} rejected by {adminEmail}");
return Ok(new { success = true, message = "Order rejected." });
}
catch (Exception ex)
{
_logger.LogError(ex, $"Error rejecting order {orderId}");
return StatusCode(500, new { error = "Internal server error" });
}
}
// --- Legacy Localhost-Only Endpoints ---
[HttpPost("api/Admin/SeedPlans")]
public async Task<IActionResult> SeedPlans([FromBody] List<Plan> plans)
{
if (!IsLocalhost()) return Forbid("Localhost only");
try
{
foreach (var plan in plans)
{
var filter = Builders<Plan>.Filter.Eq(p => p.Interval, plan.Interval);
var options = new ReplaceOptions { IsUpsert = true };
await _context.Plans.ReplaceOneAsync(filter, plan, options);
}
return Ok(new { success = true, message = "Plans seeded" });
}
catch (Exception ex)
{
return StatusCode(500, new { error = ex.Message });
}
}
[HttpGet("api/Admin/Plans")]
public async Task<IActionResult> GetPlans()
{
if (!IsLocalhost()) return Forbid("Localhost only");
var plans = await _context.Plans.Find(_ => true).ToListAsync();
return Ok(new { success = true, plans });
}
[HttpDelete("api/Admin/Plans")]
public async Task<IActionResult> DeleteAllPlans()
{
if (!IsLocalhost()) return Forbid("Localhost only");
await _context.Plans.DeleteManyAsync(_ => true);
return Ok(new { success = true });
}
}
}

View File

@ -0,0 +1,64 @@
using Microsoft.AspNetCore.Mvc;
using QRRapidoApp.Services;
namespace QRRapidoApp.Controllers
{
public class DevTutoriaisController : Controller
{
private readonly IMarkdownService _markdownService;
private readonly ILogger<DevTutoriaisController> _logger;
private const string ContentFolder = "DevTutoriais";
public DevTutoriaisController(IMarkdownService markdownService, ILogger<DevTutoriaisController> logger)
{
_markdownService = markdownService;
_logger = logger;
}
[Route("Developer/docs")]
[Route("es/Developer/docs")]
[Route("en/Developer/docs")]
public async Task<IActionResult> Index()
{
var culture = GetCulture();
var articles = await _markdownService.GetAllArticlesAsync(culture, ContentFolder);
ViewBag.Culture = culture;
return View(articles);
}
[Route("Developer/docs/{slug}")]
[Route("es/Developer/docs/{slug}")]
[Route("en/Developer/docs/{slug}")]
public async Task<IActionResult> Article(string slug)
{
var culture = GetCulture();
var article = await _markdownService.GetArticleAsync(slug, culture, ContentFolder);
if (article == null)
{
_logger.LogWarning("Dev article not found: {Slug} ({Culture})", slug, culture);
return NotFound();
}
var allArticles = await _markdownService.GetAllArticlesAsync(culture, ContentFolder);
article.RelatedArticles = allArticles
.Where(a => a.Slug != slug)
.Take(3)
.ToList();
ViewBag.Culture = culture;
ViewBag.Slug = slug;
return View(article);
}
private string GetCulture()
{
var path = Request.Path.Value ?? "";
if (path.StartsWith("/es/", StringComparison.OrdinalIgnoreCase) ||
string.Equals(path, "/es", StringComparison.OrdinalIgnoreCase)) return "es";
if (path.StartsWith("/en/", StringComparison.OrdinalIgnoreCase) ||
string.Equals(path, "/en", StringComparison.OrdinalIgnoreCase)) return "en";
return "pt-BR";
}
}
}

View File

@ -0,0 +1,162 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using QRRapidoApp.Models;
using QRRapidoApp.Services;
using System.Security.Claims;
namespace QRRapidoApp.Controllers
{
[Authorize]
[Route("Developer")]
[Route("es/Developer")]
[Route("en/Developer")]
public class DeveloperController : Controller
{
private readonly IUserService _userService;
private readonly StripeService _stripeService;
private readonly ILogger<DeveloperController> _logger;
public DeveloperController(
IUserService userService,
StripeService stripeService,
ILogger<DeveloperController> logger)
{
_userService = userService;
_stripeService = stripeService;
_logger = logger;
}
[HttpGet("")]
public async Task<IActionResult> Index()
{
var userId = User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
if (string.IsNullOrEmpty(userId)) return Unauthorized();
var user = await _userService.GetUserAsync(userId);
if (user == null) return NotFound();
ViewBag.Culture = GetCulture();
return View(user);
}
[HttpPost("GenerateKey")]
[ValidateAntiForgeryToken]
public async Task<IActionResult> GenerateKey(string keyName)
{
var userId = User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
if (string.IsNullOrEmpty(userId)) return Unauthorized();
if (string.IsNullOrWhiteSpace(keyName) || keyName.Length > 50)
{
TempData["Error"] = "Nome da chave inválido (máx. 50 caracteres).";
return Redirect(GetDevUrl());
}
var user = await _userService.GetUserAsync(userId);
if (user == null) return NotFound();
if (user.ApiKeys.Count(k => k.IsActive) >= 5)
{
TempData["Error"] = "Limite de 5 chaves ativas atingido. Revogue uma antes de criar outra.";
return Redirect(GetDevUrl());
}
var (rawKey, prefix) = await _userService.GenerateApiKeyAsync(userId, keyName.Trim());
_logger.LogInformation("API key '{Prefix}' generated for user {UserId}", prefix, userId);
TempData["NewKey"] = rawKey;
TempData["NewKeyName"] = keyName.Trim();
return RedirectToAction(nameof(Index));
}
[HttpGet("Pricing")]
public async Task<IActionResult> Pricing()
{
var userId = User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
User? user = null;
if (!string.IsNullOrEmpty(userId))
user = await _userService.GetUserAsync(userId);
ViewBag.CurrentTier = user?.ApiSubscription?.EffectiveTier ?? ApiPlanTier.Free;
ViewBag.Culture = GetCulture();
return View();
}
[HttpPost("SubscribeApi")]
[ValidateAntiForgeryToken]
public async Task<IActionResult> SubscribeApi(string planTier)
{
var userId = User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
if (string.IsNullOrEmpty(userId)) return Unauthorized();
if (!Enum.TryParse<ApiPlanTier>(planTier, out var tier) ||
tier == ApiPlanTier.Free ||
tier == ApiPlanTier.Enterprise)
{
TempData["Error"] = "Plano inválido selecionado.";
return Redirect(GetDevUrl("Pricing"));
}
try
{
var baseUrl = $"{Request.Scheme}://{Request.Host}";
var checkoutUrl = await _stripeService.CreateApiSubscriptionCheckoutAsync(userId, tier, baseUrl);
return Redirect(checkoutUrl);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error creating API subscription checkout for user {UserId}", userId);
TempData["Error"] = "Erro ao criar sessão de pagamento. Tente novamente.";
return Redirect(GetDevUrl("Pricing"));
}
}
[HttpPost("RevokeKey")]
[ValidateAntiForgeryToken]
public async Task<IActionResult> RevokeKey(string prefix)
{
var userId = User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
if (string.IsNullOrEmpty(userId)) return Unauthorized();
if (string.IsNullOrWhiteSpace(prefix))
{
TempData["Error"] = "Chave inválida.";
return Redirect(GetDevUrl());
}
var revoked = await _userService.RevokeApiKeyAsync(userId, prefix);
if (revoked)
{
_logger.LogInformation("API key '{Prefix}' revoked by user {UserId}", prefix, userId);
TempData["Success"] = "Chave revogada com sucesso.";
}
else
{
TempData["Error"] = "Chave não encontrada ou já inativa.";
}
return RedirectToAction(nameof(Index));
}
private string GetCulture()
{
var path = Request.Path.Value ?? "";
if (path.StartsWith("/es/", StringComparison.OrdinalIgnoreCase) ||
string.Equals(path, "/es", StringComparison.OrdinalIgnoreCase)) return "es";
if (path.StartsWith("/en/", StringComparison.OrdinalIgnoreCase) ||
string.Equals(path, "/en", StringComparison.OrdinalIgnoreCase)) return "en";
return "pt-BR";
}
private string GetDevUrl(string action = "")
{
var base_ = GetCulture() switch {
"es" => "/es/Developer",
"en" => "/en/Developer",
_ => "/Developer"
};
return string.IsNullOrEmpty(action) ? base_ : $"{base_}/{action}";
}
}
}

View File

@ -2,6 +2,7 @@ using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Diagnostics.HealthChecks;
using System.Diagnostics;
using System.Text.Json;
using System.Threading;
namespace QRRapidoApp.Controllers
{
@ -14,7 +15,7 @@ namespace QRRapidoApp.Controllers
private readonly ILogger<HealthController> _logger;
private readonly string _applicationName;
private readonly string _version;
private static readonly DateTime _startTime = DateTime.UtcNow;
private static readonly DateTime _startTime = System.Diagnostics.Process.GetCurrentProcess().StartTime.ToUniversalTime();
public HealthController(
HealthCheckService healthCheckService,
@ -54,7 +55,6 @@ namespace QRRapidoApp.Controllers
checks = new
{
mongodb = ExtractCheckResult(healthReport, "mongodb"),
seq = ExtractCheckResult(healthReport, "seq"),
resources = ExtractCheckResult(healthReport, "resources"),
externalServices = ExtractCheckResult(healthReport, "external_services")
},
@ -117,35 +117,6 @@ namespace QRRapidoApp.Controllers
}
}
/// <summary>
/// Seq logging service health check
/// GET /health/seq
/// </summary>
[HttpGet("seq")]
public async Task<IActionResult> GetSeqHealth()
{
try
{
var healthReport = await _healthCheckService.CheckHealthAsync(check => check.Name == "seq");
var seqCheck = healthReport.Entries.FirstOrDefault(e => e.Key == "seq");
if (seqCheck.Key == null)
{
return StatusCode(503, new { status = "unhealthy", error = "Seq health check not found" });
}
var statusCode = seqCheck.Value.Status == HealthStatus.Unhealthy ? 503 : 200;
var response = ExtractCheckResult(healthReport, "seq");
return StatusCode(statusCode, response);
}
catch (Exception ex)
{
_logger.LogError(ex, "Seq health check failed");
return StatusCode(503, new { status = "unhealthy", error = ex.Message });
}
}
/// <summary>
/// System resources health check
/// GET /health/resources
@ -258,23 +229,34 @@ namespace QRRapidoApp.Controllers
// For Uptime Kuma, we want to return 200 OK for healthy/degraded and 503 for unhealthy
var statusCode = overallStatus == "unhealthy" ? 503 : 200;
var uptimeMinutes = (DateTime.UtcNow - _startTime).TotalMinutes;
var memoryBytes = GC.GetTotalMemory(false);
var memoryMB = memoryBytes / 1024.0 / 1024.0;
var response = new
{
status = overallStatus,
application = _applicationName,
version = _version,
timestamp = DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ssZ"),
uptime = $"{(DateTime.UtcNow - _startTime).TotalHours:F1}h",
uptime = System.FormattableString.Invariant($"{(DateTime.UtcNow - _startTime).TotalHours:F1}h"),
// Include critical metrics for monitoring
metrics = new
{
mongodb_connected = GetCheckStatus(healthReport, "mongodb") != "unhealthy",
seq_reachable = GetCheckStatus(healthReport, "seq") != "unhealthy",
resources_ok = GetCheckStatus(healthReport, "resources") != "unhealthy",
external_services_ok = GetCheckStatus(healthReport, "external_services") != "unhealthy"
mongodb_connected = GetCheckStatus(healthReport, "mongodb") == "healthy",
resources_ok = GetCheckStatus(healthReport, "resources") == "healthy",
external_services_ok = GetCheckStatus(healthReport, "external_services") == "healthy"
}
};
_logger.LogInformation("[HEALTH] Memory: {memoryBytes} MemoryMB: {memoryMB} ThreadPool: {threadPool} ProcessId: {processId} ActiveConnections: {activeConnections} UptimeMinutes: {uptimeMinutes}",
memoryBytes,
memoryMB,
ThreadPool.ThreadCount,
Process.GetCurrentProcess().Id,
HttpContext.Connection?.Id ?? "null",
Math.Round(uptimeMinutes, 1));
return StatusCode(statusCode, response);
}
catch (Exception ex)

View File

@ -1,128 +1,404 @@
using Microsoft.AspNetCore.Mvc;
using QRRapidoApp.Models;
using QRRapidoApp.Services;
using System.Diagnostics;
using System.Security.Claims;
namespace QRRapidoApp.Controllers
{
public class HomeController : Controller
{
private readonly ILogger<HomeController> _logger;
private readonly AdDisplayService _adDisplayService;
private readonly IUserService _userService;
private readonly IConfiguration _config;
public HomeController(ILogger<HomeController> logger, AdDisplayService adDisplayService, IUserService userService, IConfiguration config)
{
_logger = logger;
_adDisplayService = adDisplayService;
_userService = userService;
_config = config;
}
public async Task<IActionResult> Index()
{
var userId = User?.FindFirst(ClaimTypes.NameIdentifier)?.Value;
ViewBag.ShowAds = await _adDisplayService.ShouldShowAds(userId);
ViewBag.IsPremium = await _adDisplayService.HasValidPremiumSubscription(userId ?? "");
ViewBag.IsAuthenticated = User.Identity?.IsAuthenticated ?? false;
ViewBag.UserName = User.Identity?.Name ?? "";
// SEO and Analytics data
ViewBag.Title = _config["App:TaglinePT"];
ViewBag.Keywords = _config["SEO:KeywordsPT"];
ViewBag.Description = "QR Rapido: Gere códigos QR em segundos! Gerador ultrarrápido em português e espanhol. Grátis, sem cadastro obrigatório. 30 dias sem anúncios após login.";
// User stats for logged in users
if (!string.IsNullOrEmpty(userId))
{
ViewBag.DailyQRCount = await _userService.GetDailyQRCountAsync(userId);
ViewBag.MonthlyQRCount = await _userService.GetQRCountThisMonthAsync(userId);
}
return View();
}
public IActionResult Privacy()
{
return View();
}
public IActionResult Terms()
{
return View();
}
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
public IActionResult Error()
{
return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
}
// Dynamic QR redirect endpoint
[Route("d/{id}")]
public async Task<IActionResult> DynamicRedirect(string id)
{
try
{
// This would lookup the dynamic QR content from cache/database
// For now, return a placeholder
return Redirect("https://qrrapido.site");
}
catch
{
return NotFound();
}
}
// Health check endpoint
[Route("health")]
public IActionResult Health()
{
return Ok(new { status = "healthy", timestamp = DateTime.UtcNow });
}
// Sitemap endpoint for SEO
[Route("sitemap.xml")]
public IActionResult Sitemap()
{
var sitemap = $@"<?xml version=""1.0"" encoding=""UTF-8""?>
<urlset xmlns=""http://www.sitemaps.org/schemas/sitemap/0.9"">
<url>
<loc>https://qrrapido.site/</loc>
<lastmod>{DateTime.UtcNow:yyyy-MM-dd}</lastmod>
<changefreq>daily</changefreq>
<priority>1.0</priority>
</url>
<url>
<loc>https://qrrapido.site/pt/</loc>
<lastmod>{DateTime.UtcNow:yyyy-MM-dd}</lastmod>
<changefreq>daily</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://qrrapido.site/es/</loc>
<lastmod>{DateTime.UtcNow:yyyy-MM-dd}</lastmod>
<changefreq>daily</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://qrrapido.site/Premium/Upgrade</loc>
<lastmod>{DateTime.UtcNow:yyyy-MM-dd}</lastmod>
<changefreq>weekly</changefreq>
<priority>0.8</priority>
</url>
</urlset>";
return Content(sitemap, "application/xml");
}
}
public class ErrorViewModel
{
public string? RequestId { get; set; }
public bool ShowRequestId => !string.IsNullOrEmpty(RequestId);
}
}
using Microsoft.AspNetCore.Mvc;
using QRRapidoApp.Models;
using QRRapidoApp.Services;
using System.Diagnostics;
using System.Security.Claims;
using Microsoft.Extensions.Localization;
namespace QRRapidoApp.Controllers
{
public class HomeController : Controller
{
private readonly ILogger<HomeController> _logger;
private readonly AdDisplayService _adDisplayService;
private readonly IUserService _userService;
private readonly IConfiguration _config;
private readonly IStringLocalizer<QRRapidoApp.Resources.SharedResource> _localizer;
private readonly IMarkdownService _markdownService;
public HomeController(
ILogger<HomeController> logger,
AdDisplayService adDisplayService,
IUserService userService,
IConfiguration config,
IStringLocalizer<QRRapidoApp.Resources.SharedResource> localizer,
IMarkdownService markdownService
)
{
_logger = logger;
_adDisplayService = adDisplayService;
_userService = userService;
_config = config;
_localizer = localizer;
_markdownService = markdownService;
}
// Home page routes
// "/" → Portuguese (canonical)
// "/es" → Spanish
[HttpGet]
[Route("/")]
[Route("es")]
[Route("en")]
public async Task<IActionResult> Index(string? qrType = null)
{
var userId = User?.FindFirst(ClaimTypes.NameIdentifier)?.Value;
// Pass the requested QR type to the view to auto-select in combo
ViewBag.SelectedQRType = qrType;
// Set SEO Meta Tags based on QR Type
if (!string.IsNullOrEmpty(qrType))
{
switch (qrType.ToLower())
{
case "pix":
ViewBag.Title = "Gerador de Pix Grátis";
ViewBag.Description = "Crie QR Code PIX estático gratuitamente. Ideal para receber pagamentos e doações.";
ViewBag.Keywords = "pix, qr code pix, gerador pix, pix estatico, receber pix";
break;
case "wifi":
ViewBag.Title = _localizer["WiFiQRTitle"];
ViewBag.Description = _localizer["WiFiQRDescription"];
break;
case "vcard":
ViewBag.Title = _localizer["VCardQRTitle"] ?? "Gerador de Cartão de Visita Digital";
ViewBag.Description = _localizer["VCardQRDescription"];
break;
case "whatsapp":
ViewBag.Title = "Gerador de Link WhatsApp";
ViewBag.Description = _localizer["WhatsAppQRDescription"];
break;
case "email":
ViewBag.Title = "Gerador de QR Code Email";
ViewBag.Description = _localizer["EmailQRDescription"];
break;
case "sms":
ViewBag.Title = "Gerador de QR Code SMS";
ViewBag.Description = _localizer["SMSQRDescription"];
break;
case "text":
ViewBag.Title = "Gerador de Texto para QR";
ViewBag.Description = _localizer["TextQRDescription"];
break;
case "url":
ViewBag.Title = "Gerador de URL para QR";
ViewBag.Description = _localizer["URLQRDescription"];
break;
}
}
else
{
// Default SEO
ViewBag.Title = _config["App:TaglinePT"];
ViewBag.Keywords = _config["SEO:KeywordsPT"];
ViewBag.Description = _localizer["QRGenerateDescription"];
}
ViewBag.ShowAds = await _adDisplayService.ShouldShowAds(userId);
ViewBag.IsPremium = await _adDisplayService.HasValidPremiumSubscription(userId ?? "");
ViewBag.IsAuthenticated = User?.Identity?.IsAuthenticated ?? false;
ViewBag.UserName = User?.Identity?.Name ?? "";
_adDisplayService.SetViewBagAds(ViewBag);
// User stats for logged in users
if (!string.IsNullOrEmpty(userId))
{
ViewBag.DailyQRCount = await _userService.GetDailyQRCountAsync(userId);
ViewBag.MonthlyQRCount = await _userService.GetQRCountThisMonthAsync(userId);
}
return View("Index");
}
// Dedicated SEO Routes - Landing pages for each QR type
// Portuguese (default): /pix, /wifi, etc. (no culture prefix)
// Spanish: /es/pix, /es/wifi, etc.
[Route("pix")]
[Route("es/pix")]
[Route("en/pix")]
public async Task<IActionResult> Pix() => await Index("pix");
[Route("wifi")]
[Route("es/wifi")]
[Route("en/wifi")]
public async Task<IActionResult> Wifi() => await Index("wifi");
[Route("vcard")]
[Route("es/vcard")]
[Route("en/vcard")]
public async Task<IActionResult> VCard() => await Index("vcard");
[Route("whatsapp")]
[Route("es/whatsapp")]
[Route("en/whatsapp")]
public async Task<IActionResult> WhatsApp() => await Index("whatsapp");
[Route("email")]
[Route("es/email")]
[Route("en/email")]
public async Task<IActionResult> Email() => await Index("email");
[Route("sms")]
[Route("es/sms")]
[Route("en/sms")]
public async Task<IActionResult> Sms() => await Index("sms");
[Route("texto")]
[Route("text")]
[Route("es/texto")]
[Route("es/text")]
[Route("en/text")]
public async Task<IActionResult> Text() => await Index("text");
[Route("url")]
[Route("es/url")]
[Route("en/url")]
public async Task<IActionResult> UrlType() => await Index("url");
public IActionResult Privacy()
{
var userId = User?.FindFirst(ClaimTypes.NameIdentifier)?.Value;
ViewBag.ShowAds = _adDisplayService.ShouldShowAds(userId).Result;
ViewBag.IsPremium = _adDisplayService.HasValidPremiumSubscription(userId ?? "").Result;
ViewBag.IsAuthenticated = User.Identity?.IsAuthenticated ?? false;
ViewBag.UserName = User.Identity?.Name ?? "";
ViewBag.Title = _localizer["PrivacyPolicyTitle"];
ViewBag.Description = _localizer["PrivacyPolicyDescription"];
_adDisplayService.SetViewBagAds(ViewBag);
return View();
}
public IActionResult Terms()
{
var userId = User?.FindFirst(ClaimTypes.NameIdentifier)?.Value;
ViewBag.ShowAds = _adDisplayService.ShouldShowAds(userId).Result;
ViewBag.IsPremium = _adDisplayService.HasValidPremiumSubscription(userId ?? "").Result;
ViewBag.IsAuthenticated = User.Identity?.IsAuthenticated ?? false;
ViewBag.UserName = User.Identity?.Name ?? "";
ViewBag.Title = _localizer["TermsOfUseTitle"];
ViewBag.Description = _localizer["TermsOfUseDescription"];
_adDisplayService.SetViewBagAds(ViewBag);
return View();
}
public IActionResult About()
{
var userId = User?.FindFirst(ClaimTypes.NameIdentifier)?.Value;
ViewBag.ShowAds = _adDisplayService.ShouldShowAds(userId).Result;
ViewBag.IsPremium = _adDisplayService.HasValidPremiumSubscription(userId ?? "").Result;
ViewBag.IsAuthenticated = User.Identity?.IsAuthenticated ?? false;
ViewBag.UserName = User.Identity?.Name ?? "";
ViewBag.Title = _localizer["AboutPageTitle"];
ViewBag.Description = _localizer["AboutPageDescription"];
_adDisplayService.SetViewBagAds(ViewBag);
return View();
}
public IActionResult Contact()
{
var userId = User?.FindFirst(ClaimTypes.NameIdentifier)?.Value;
ViewBag.ShowAds = _adDisplayService.ShouldShowAds(userId).Result;
ViewBag.IsPremium = _adDisplayService.HasValidPremiumSubscription(userId ?? "").Result;
ViewBag.IsAuthenticated = User.Identity?.IsAuthenticated ?? false;
ViewBag.UserName = User.Identity?.Name ?? "";
ViewBag.Title = _localizer["ContactPageTitle"];
ViewBag.Description = _localizer["ContactPageDescription"];
_adDisplayService.SetViewBagAds(ViewBag);
return View();
}
public IActionResult FAQ()
{
var userId = User?.FindFirst(ClaimTypes.NameIdentifier)?.Value;
ViewBag.ShowAds = _adDisplayService.ShouldShowAds(userId).Result;
ViewBag.IsPremium = _adDisplayService.HasValidPremiumSubscription(userId ?? "").Result;
ViewBag.IsAuthenticated = User.Identity?.IsAuthenticated ?? false;
ViewBag.UserName = User.Identity?.Name ?? "";
ViewBag.Title = _localizer["FAQPageTitle"];
ViewBag.Description = _localizer["FAQPageDescription"];
_adDisplayService.SetViewBagAds(ViewBag);
return View();
}
public IActionResult HowToUse()
{
var userId = User?.FindFirst(ClaimTypes.NameIdentifier)?.Value;
ViewBag.ShowAds = _adDisplayService.ShouldShowAds(userId).Result;
ViewBag.IsPremium = _adDisplayService.HasValidPremiumSubscription(userId ?? "").Result;
ViewBag.IsAuthenticated = User.Identity?.IsAuthenticated ?? false;
ViewBag.UserName = User.Identity?.Name ?? "";
ViewBag.Title = _localizer["HowToUsePageTitle"];
ViewBag.Description = _localizer["HowToUsePageDescription"];
_adDisplayService.SetViewBagAds(ViewBag);
return View();
}
//[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
//public IActionResult Error()
//{
// return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
//}
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
public IActionResult Error()
{
var errorCode = Request.Query["code"].ToString();
var errorMessage = "";
// Interpretar c<>digos de erro espec<65>ficos
if (errorCode.StartsWith("M.C506"))
{
errorMessage = "Erro de autentica<63><61>o. Verifique suas credenciais e tente novamente.";
}
ViewBag.ErrorCode = errorCode;
ViewBag.ErrorMessage = errorMessage;
return View(new ErrorViewModel
{
RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier
});
}
// Dynamic QR redirect endpoint
[Route("d/{id}")]
public async Task<IActionResult> DynamicRedirect(string id)
{
try
{
_adDisplayService.SetViewBagAds(ViewBag);
// This would lookup the dynamic QR content from cache/database
// For now, return a placeholder
return Redirect("https://qrrapido.site");
}
catch
{
return NotFound();
}
}
// Health check endpoint
[Route("health")]
public IActionResult Health()
{
return Ok(new { status = "healthy", timestamp = DateTime.UtcNow });
}
// Sitemap endpoint for SEO
[Route("sitemap.xml")]
public async Task<IActionResult> Sitemap()
{
var baseUrl = $"{Request.Scheme}://{Request.Host}";
var now = DateTime.UtcNow.ToString("yyyy-MM-dd");
var sitemapBuilder = new System.Text.StringBuilder();
sitemapBuilder.AppendLine(@"<?xml version=""1.0"" encoding=""UTF-8""?>");
sitemapBuilder.AppendLine(@"<urlset xmlns=""http://www.sitemaps.org/schemas/sitemap/0.9"">");
void AppendUrl(string relativePath, string changeFreq, string priority, string? lastModOverride = null)
{
var normalizedPath = relativePath.StartsWith("/")
? relativePath
: $"/{relativePath}";
var lastModValue = lastModOverride ?? now;
sitemapBuilder.AppendLine($@"
<url>
<loc>{baseUrl}{normalizedPath}</loc>
<lastmod>{lastModValue}</lastmod>
<changefreq>{changeFreq}</changefreq>
<priority>{priority}</priority>
</url>");
}
// Core entry points
AppendUrl("/", "daily", "1.0");
AppendUrl("/es", "daily", "0.9");
// Tools (Landing Pages)
var tools = new[] { "pix", "wifi", "vcard", "whatsapp", "email", "sms", "texto", "url" };
foreach (var tool in tools)
{
AppendUrl($"/{tool}", "weekly", "0.9");
AppendUrl($"/es/{tool}", "weekly", "0.8");
}
// Informational pages
var informationalPages = new[]
{
new { Path = "Home/About", ChangeFreq = "monthly", Priority = "0.7" },
new { Path = "Home/Contact", ChangeFreq = "monthly", Priority = "0.7" },
new { Path = "Home/FAQ", ChangeFreq = "weekly", Priority = "0.8" },
new { Path = "Home/HowToUse", ChangeFreq = "weekly", Priority = "0.8" },
new { Path = "Home/Privacy", ChangeFreq = "monthly", Priority = "0.4" },
new { Path = "Home/Terms", ChangeFreq = "monthly", Priority = "0.4" }
};
foreach (var page in informationalPages)
{
AppendUrl($"/{page.Path}", page.ChangeFreq, page.Priority);
AppendUrl($"/es/{page.Path}", page.ChangeFreq, page.Priority);
}
// Dynamic tutorial pages from Markdown
try
{
var allArticles = await _markdownService.GetAllArticlesForSitemapAsync();
foreach (var article in allArticles)
{
// Use slug from metadata or generate from title if missing
var slug = !string.IsNullOrWhiteSpace(article.Slug)
? article.Slug
: article.Title.ToLower().Replace(" ", "-");
// Ensure slug is only encoded once and special characters are handled
var encodedSlug = slug; // Assuming slug is already URL-safe from filename
var tutorialPath = article.Culture == "pt-BR"
? $"/tutoriais/{encodedSlug}"
: $"/es/tutoriais/{encodedSlug}";
var lastMod = article.LastMod.ToString("yyyy-MM-dd");
AppendUrl(tutorialPath, "weekly", "0.8", lastMod);
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Error adding tutorials to sitemap");
}
sitemapBuilder.AppendLine("</urlset>");
return Content(sitemapBuilder.ToString(), "application/xml", System.Text.Encoding.UTF8);
}
}
//public class ErrorViewModel
//{
// public string? RequestId { get; set; }
// public bool ShowRequestId => !string.IsNullOrEmpty(RequestId);
//}
}

View File

@ -1,120 +1,287 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using QRRapidoApp.Services;
using QRRapidoApp.Models;
using System.Security.Claims;
using System.Threading.Tasks;
using QRRapidoApp.Models.ViewModels;
using System.Linq;
using MongoDB.Driver;
using QRRapidoApp.Data;
using System.Text;
using Stripe.Checkout;
namespace QRRapidoApp.Controllers
{
[Authorize]
public class PagamentoController : Controller
{
private readonly IPlanService _planService;
private readonly IUserService _userService;
private readonly StripeService _stripeService;
private readonly ILogger<PagamentoController> _logger;
private readonly MongoDbContext _context;
private readonly AdDisplayService _adDisplayService;
private readonly StripeService _stripeService; // Injected StripeService
private readonly string _pixKey = "12048391000101";
private readonly string _merchantName = "RRCG Gerenciamento";
private readonly string _merchantCity = "SAO PAULO";
public PagamentoController(IPlanService planService, IUserService userService, StripeService stripeService, ILogger<PagamentoController> logger)
public PagamentoController(
IUserService userService,
ILogger<PagamentoController> logger,
MongoDbContext context,
AdDisplayService adDisplayService,
IConfiguration config,
StripeService stripeService)
{
_planService = planService;
_userService = userService;
_stripeService = stripeService;
_logger = logger;
_context = context;
_adDisplayService = adDisplayService;
_stripeService = stripeService;
var configPixKey = config["Payment:PixKey"];
if (!string.IsNullOrEmpty(configPixKey))
{
_pixKey = configPixKey;
}
}
[HttpGet]
public async Task<IActionResult> SelecaoPlano()
{
var plans = await _planService.GetActivePlansAsync();
var countryCode = GetUserCountryCode(); // Implement this method based on your needs
_adDisplayService.SetViewBagAds(ViewBag);
// Definição dos pacotes com PREÇOS DIFERENCIADOS
var packages = GetPackages();
var model = new SelecaoPlanoViewModel
{
Plans = plans,
CountryCode = countryCode
};
return View(model);
return View(packages);
}
[HttpPost]
public async Task<IActionResult> CreateCheckout(string planId)
[HttpPost("api/Pagamento/CreatePixOrder")]
public async Task<IActionResult> CreatePixOrder([FromBody] CreateOrderRequest request)
{
var userId = User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
if (string.IsNullOrEmpty(userId))
var userEmail = User.FindFirst(ClaimTypes.Email)?.Value;
if (string.IsNullOrEmpty(userId)) return Unauthorized();
var package = GetPackage(request.PackageId);
if (package == null) return BadRequest("Pacote inválido");
try
{
return Json(new { success = false, error = "User not authenticated" });
}
var plan = await _planService.GetPlanByIdAsync(planId);
if (plan == null)
{
return Json(new { success = false, error = "Plan not found" });
}
var countryCode = GetUserCountryCode();
var priceId = plan.PricesByCountry.ContainsKey(countryCode)
? plan.PricesByCountry[countryCode].StripePriceId
: plan.StripePriceId;
try
{
var checkoutUrl = await _stripeService.CreateCheckoutSessionAsync(userId, priceId);
return Json(new { success = true, url = checkoutUrl });
}
catch (Exception ex)
{
_logger.LogError(ex, $"Error creating checkout session for user {userId} and plan {planId}");
return Json(new { success = false, error = ex.Message });
}
}
[HttpGet]
public IActionResult Sucesso()
{
ViewBag.SuccessMessage = "Pagamento concluído com sucesso! Bem-vindo ao Premium.";
return View();
}
[HttpGet]
public IActionResult Cancelar()
{
ViewBag.CancelMessage = "O pagamento foi cancelado. Você pode tentar novamente a qualquer momento.";
return View("SelecaoPlano");
}
[HttpPost]
[AllowAnonymous]
public async Task<IActionResult> StripeWebhook()
{
try
{
using var reader = new StreamReader(HttpContext.Request.Body);
var json = await reader.ReadToEndAsync();
var signature = Request.Headers["Stripe-Signature"].FirstOrDefault();
if (string.IsNullOrEmpty(signature))
// Create Order (PIX Price)
var order = new Order
{
return BadRequest("Missing Stripe signature");
}
UserId = userId,
UserEmail = userEmail ?? "unknown",
Amount = package.PricePix,
CreditsAmount = package.Credits,
Status = "Pending",
CreatedAt = DateTime.UtcNow
};
await _stripeService.HandleWebhookAsync(json, signature);
return Ok();
await _context.Orders.InsertOneAsync(order);
var shortId = order.Id.Substring(order.Id.Length - 8).ToUpper();
var txId = $"PED{shortId}";
var update = Builders<Order>.Update.Set(o => o.PixCode, txId);
await _context.Orders.UpdateOneAsync(o => o.Id == order.Id, update);
var pixPayload = PixPayloadGenerator.GeneratePayload(
_pixKey,
package.PricePix,
_merchantName,
_merchantCity,
txId
);
return Ok(new
{
success = true,
pixCode = pixPayload,
orderId = txId,
amount = package.PricePix,
credits = package.Credits
});
}
catch (Exception ex)
{
_logger.LogError(ex, "Error processing Stripe webhook");
return BadRequest(ex.Message);
_logger.LogError(ex, "Erro ao gerar pedido PIX");
return StatusCode(500, new { success = false, error = "Erro interno" });
}
}
private string GetUserCountryCode()
[HttpPost("api/Pagamento/CreateStripeSession")]
public async Task<IActionResult> CreateStripeSession([FromBody] CreateOrderRequest request)
{
// Prioritize Cloudflare header, fallback to a default or other methods
return HttpContext.Request.Headers["CF-IPCountry"].FirstOrDefault() ?? "BR";
var userId = User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
if (string.IsNullOrEmpty(userId)) return Unauthorized();
var package = GetPackage(request.PackageId);
if (package == null) return BadRequest("Pacote inválido");
try
{
// Create Stripe Checkout Session (One-Time Payment)
// We create an ad-hoc price on the fly using line_items
var options = new SessionCreateOptions
{
PaymentMethodTypes = new List<string> { "card" },
Mode = "payment", // One-time payment
LineItems = new List<SessionLineItemOptions>
{
new SessionLineItemOptions
{
PriceData = new SessionLineItemPriceDataOptions
{
Currency = "brl",
UnitAmount = (long)(package.PriceCard * 100), // Centavos
ProductData = new SessionLineItemPriceDataProductDataOptions
{
Name = $"{package.Credits} Créditos QR Rapido",
Description = package.Description
}
},
Quantity = 1,
},
},
Metadata = new Dictionary<string, string>
{
{ "user_id", userId },
{ "credits_amount", package.Credits.ToString() },
{ "package_id", package.Id }
},
SuccessUrl = $"{Request.Scheme}://{Request.Host}/Pagamento/Sucesso",
CancelUrl = $"{Request.Scheme}://{Request.Host}/Pagamento/SelecaoPlano",
};
var service = new SessionService();
var session = await service.CreateAsync(options);
return Ok(new { success = true, url = session.Url });
}
catch (Exception ex)
{
_logger.LogError(ex, "Stripe session error");
return StatusCode(500, new { success = false, error = ex.Message });
}
}
private List<CreditPackageViewModel> GetPackages()
{
return new List<CreditPackageViewModel>
{
new CreditPackageViewModel {
Id = "starter",
Name = "Iniciante",
Credits = 10,
PricePix = 5.00m, // R$ 0,50/un
PriceCard = 6.00m,
Description = "Ideal para testes rápidos",
Savings = 0
},
new CreditPackageViewModel {
Id = "pro",
Name = "Profissional",
Credits = 50,
PricePix = 22.50m, // R$ 0,45/un (Desconto de volume)
PriceCard = 27.00m,
Description = "Para uso recorrente",
Savings = 10,
IsPopular = true
},
new CreditPackageViewModel {
Id = "agency",
Name = "Agência",
Credits = 100,
PricePix = 40.00m, // R$ 0,40/un (Super desconto)
PriceCard = 48.00m,
Description = "Volume alto com desconto máximo",
Savings = 20
}
};
}
private CreditPackageViewModel? GetPackage(string id)
{
return GetPackages().FirstOrDefault(p => p.Id == id);
}
public class CreateOrderRequest
{
public string PackageId { get; set; }
}
public class CreditPackageViewModel
{
public string Id { get; set; }
public string Name { get; set; }
public int Credits { get; set; }
public decimal PricePix { get; set; } // Preço PIX
public decimal PriceCard { get; set; } // Preço Cartão (+taxas)
public string Description { get; set; }
public int Savings { get; set; }
public bool IsPopular { get; set; }
}
// --- Helper Interno para Gerar PIX (CRC16) ---
public static class PixPayloadGenerator
{
public static string GeneratePayload(string pixKey, decimal amount, string merchantName, string merchantCity, string txId)
{
var sb = new StringBuilder();
sb.Append(FormatField("00", "01"));
var merchantInfo = new StringBuilder();
merchantInfo.Append(FormatField("00", "br.gov.bcb.pix"));
merchantInfo.Append(FormatField("01", pixKey));
sb.Append(FormatField("26", merchantInfo.ToString()));
sb.Append(FormatField("52", "0000"));
sb.Append(FormatField("53", "986"));
sb.Append(FormatField("54", amount.ToString("F2", System.Globalization.CultureInfo.InvariantCulture)));
sb.Append(FormatField("58", "BR"));
var name = merchantName.Length > 25 ? merchantName.Substring(0, 25) : merchantName;
sb.Append(FormatField("59", RemoveAccents(name)));
var city = merchantCity.Length > 15 ? merchantCity.Substring(0, 15) : merchantCity;
sb.Append(FormatField("60", RemoveAccents(city)));
var additionalData = new StringBuilder();
additionalData.Append(FormatField("05", txId));
sb.Append(FormatField("62", additionalData.ToString()));
sb.Append("6304");
var payloadWithoutCrc = sb.ToString();
var crc = CalculateCRC16(payloadWithoutCrc);
return payloadWithoutCrc + crc;
}
private static string FormatField(string id, string value) => $"{id}{value.Length:D2}{value}";
private static string RemoveAccents(string text)
{
return text.ToUpper()
.Replace("Ã", "A").Replace("Á", "A").Replace("Â", "A")
.Replace("É", "E").Replace("Ê", "E")
.Replace("Í", "I")
.Replace("Ó", "O").Replace("Ô", "O").Replace("Õ", "O")
.Replace("Ú", "U")
.Replace("Ç", "C");
}
private static string CalculateCRC16(string data)
{
ushort crc = 0xFFFF;
byte[] bytes = Encoding.ASCII.GetBytes(data);
foreach (byte b in bytes)
{
crc ^= (ushort)(b << 8);
for (int i = 0; i < 8; i++)
{
if ((crc & 0x8000) != 0) crc = (ushort)((crc << 1) ^ 0x1021);
else crc <<= 1;
}
}
return crc.ToString("X4");
}
}
}
}
}

View File

@ -15,7 +15,12 @@ namespace QRRapidoApp.Controllers
private readonly IConfiguration _config;
private readonly ILogger<PremiumController> _logger;
public PremiumController(StripeService stripeService, IUserService userService, AdDisplayService adDisplayService, IConfiguration config, ILogger<PremiumController> logger)
public PremiumController(
StripeService stripeService,
IUserService userService,
AdDisplayService adDisplayService,
IConfiguration config,
ILogger<PremiumController> logger)
{
_stripeService = stripeService;
_userService = userService;
@ -27,12 +32,14 @@ namespace QRRapidoApp.Controllers
[HttpGet]
public IActionResult Upgrade()
{
_adDisplayService.SetViewBagAds(ViewBag);
return RedirectToAction("SelecaoPlano", "Pagamento");
}
[HttpGet]
public async Task<IActionResult> Dashboard()
{
_adDisplayService.SetViewBagAds(ViewBag);
var userId = User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
if (string.IsNullOrEmpty(userId))
{
@ -103,9 +110,40 @@ namespace QRRapidoApp.Controllers
}
}
[HttpPost]
public async Task<IActionResult> RequestRefund()
{
var userId = User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
if (string.IsNullOrEmpty(userId))
{
return Json(new { success = false, error = "Usuário não autenticado" });
}
try
{
var (success, message) = await _stripeService.CancelAndRefundSubscriptionAsync(userId);
if (success)
{
TempData["Success"] = message;
return Json(new { success = true, message = message });
}
else
{
return Json(new { success = false, error = message });
}
}
catch (Exception ex)
{
_logger.LogError(ex, $"Error processing refund for user {userId}");
return Json(new { success = false, error = "Erro inesperado ao processar reembolso" });
}
}
[HttpGet]
public async Task<IActionResult> BillingPortal()
{
_adDisplayService.SetViewBagAds(ViewBag);
var userId = User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
if (string.IsNullOrEmpty(userId))
{

View File

@ -1,9 +1,14 @@
using Microsoft.AspNetCore.Mvc;
using QRRapidoApp.Models.ViewModels;
using QRRapidoApp.Models.DTOs;
using QRRapidoApp.Services;
using System.Diagnostics;
using System.Security.Claims;
using System.Text;
using System.Security.Cryptography;
using Microsoft.Extensions.Localization;
using QRRapidoApp.Models;
using MongoDB.Driver;
namespace QRRapidoApp.Controllers
{
@ -11,473 +16,183 @@ namespace QRRapidoApp.Controllers
[Route("api/[controller]")]
public class QRController : ControllerBase
{
private readonly IQRCodeService _qrService;
private readonly IQRBusinessManager _qrBusinessManager;
private readonly IUserService _userService;
private readonly AdDisplayService _adService;
private readonly IQRCodeService _qrService; // Mantido para Download
private readonly ILogger<QRController> _logger;
private readonly IStringLocalizer<QRRapidoApp.Resources.SharedResource> _localizer;
public QRController(IQRCodeService qrService, IUserService userService, AdDisplayService adService, ILogger<QRController> logger)
public QRController(
IQRBusinessManager qrBusinessManager,
IUserService userService,
IQRCodeService qrService,
ILogger<QRController> logger,
IStringLocalizer<QRRapidoApp.Resources.SharedResource> localizer)
{
_qrService = qrService;
_qrBusinessManager = qrBusinessManager;
_userService = userService;
_adService = adService;
_qrService = qrService;
_logger = logger;
_localizer = localizer;
}
[HttpPost("GenerateRapid")]
public async Task<IActionResult> GenerateRapid([FromBody] QRGenerationRequest request)
{
var stopwatch = Stopwatch.StartNew();
var requestId = Guid.NewGuid().ToString("N")[..8];
var userId = User?.FindFirst(ClaimTypes.NameIdentifier)?.Value;
var isAuthenticated = User?.Identity?.IsAuthenticated ?? false;
using (_logger.BeginScope(new Dictionary<string, object>
var context = new UserRequesterContext
{
["RequestId"] = requestId,
["UserId"] = userId ?? "anonymous",
["IsAuthenticated"] = isAuthenticated,
["QRType"] = request.Type ?? "unknown",
["ContentLength"] = request.Content?.Length ?? 0,
["QRGeneration"] = true
}))
UserId = User?.FindFirst(ClaimTypes.NameIdentifier)?.Value,
IpAddress = HttpContext.Connection.RemoteIpAddress?.ToString() ?? "unknown",
DeviceId = Request.Cookies["_qr_device_id"]
};
var result = await _qrBusinessManager.ProcessGenerationAsync(request, context);
if (!result.Success)
{
_logger.LogInformation("QR generation request started - Type: {QRType}, ContentLength: {ContentLength}, User: {UserType}",
request.Type, request.Content?.Length ?? 0, isAuthenticated ? "authenticated" : "anonymous");
try
if (result.ErrorCode == "LIMIT_REACHED")
{
// Quick validations
if (string.IsNullOrWhiteSpace(request.Content))
{
_logger.LogWarning("QR generation failed - empty content provided");
return BadRequest(new { error = "Conteúdo é obrigatório", success = false });
}
if (request.Content.Length > 4000) // Limit to maintain speed
{
_logger.LogWarning("QR generation failed - content too long: {ContentLength} characters", request.Content.Length);
return BadRequest(new { error = "Conteúdo muito longo. Máximo 4000 caracteres.", success = false });
}
// Check user status
var user = await _userService.GetUserAsync(userId);
// Validate premium features
if (!string.IsNullOrEmpty(request.CornerStyle) && request.CornerStyle != "square" && user?.IsPremium != true)
{
_logger.LogWarning("Custom corner style attempted by non-premium user - UserId: {UserId}, CornerStyle: {CornerStyle}",
userId ?? "anonymous", request.CornerStyle);
return BadRequest(new
{
error = "Estilos de borda personalizados são exclusivos do plano Premium. Faça upgrade para usar esta funcionalidade.",
requiresPremium = true,
success = false
});
}
// Rate limiting for free users
var rateLimitPassed = await CheckRateLimitAsync(userId, user);
if (!rateLimitPassed)
{
_logger.LogWarning("QR generation rate limited - User: {UserId}, IsPremium: {IsPremium}",
userId ?? "anonymous", user?.IsPremium ?? false);
return StatusCode(429, new
{
error = "Limite de QR codes atingido",
upgradeUrl = "/Premium/Upgrade",
success = false
});
}
// Configure optimizations based on user
request.IsPremium = user?.IsPremium == true;
request.OptimizeForSpeed = true;
_logger.LogDebug("Generating QR code - IsPremium: {IsPremium}, OptimizeForSpeed: {OptimizeForSpeed}",
request.IsPremium, request.OptimizeForSpeed);
// Generate QR code
var generationStopwatch = Stopwatch.StartNew();
var result = await _qrService.GenerateRapidAsync(request);
generationStopwatch.Stop();
if (!result.Success)
{
_logger.LogError("QR generation failed - Error: {ErrorMessage}, GenerationTime: {GenerationTimeMs}ms",
result.ErrorMessage, generationStopwatch.ElapsedMilliseconds);
return StatusCode(500, new { error = result.ErrorMessage, success = false });
}
_logger.LogInformation("QR code generated successfully - GenerationTime: {GenerationTimeMs}ms, FromCache: {FromCache}, Size: {Size}px",
generationStopwatch.ElapsedMilliseconds, result.FromCache, request.Size);
// Update counter for free users
if (!request.IsPremium && userId != null)
{
var remaining = await _userService.DecrementDailyQRCountAsync(userId);
result.RemainingQRs = remaining;
_logger.LogDebug("Updated QR count for free user - Remaining: {RemainingQRs}", remaining);
}
// Save to history if user is logged in (fire and forget)
if (userId != null)
{
_ = Task.Run(async () =>
{
try
{
await _userService.SaveQRToHistoryAsync(userId, result);
_logger.LogDebug("QR code saved to history successfully for user {UserId}", userId);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error saving QR to history for user {UserId}", userId);
}
});
}
stopwatch.Stop();
var totalTimeMs = stopwatch.ElapsedMilliseconds;
// Performance logging with structured data
using (_logger.BeginScope(new Dictionary<string, object>
{
["TotalRequestTimeMs"] = totalTimeMs,
["QRGenerationTimeMs"] = generationStopwatch.ElapsedMilliseconds,
["ServiceGenerationTimeMs"] = result.GenerationTimeMs,
["FromCache"] = result.FromCache,
["UserType"] = request.IsPremium ? "premium" : "free",
["QRSize"] = request.Size,
["Success"] = true
}))
{
var performanceStatus = totalTimeMs switch
{
< 500 => "excellent",
< 1000 => "good",
< 2000 => "acceptable",
_ => "slow"
};
_logger.LogInformation("QR generation completed - TotalTime: {TotalTimeMs}ms, ServiceTime: {ServiceTimeMs}ms, Performance: {PerformanceStatus}, Cache: {FromCache}",
totalTimeMs, result.GenerationTimeMs, performanceStatus, result.FromCache);
}
return Ok(result);
return StatusCode(429, new
{
error = result.Message,
upgradeUrl = "/Account/Login"
});
}
catch (Exception ex)
if (result.ErrorCode == "INSUFFICIENT_CREDITS")
{
stopwatch.Stop();
_logger.LogError(ex, "QR generation failed with exception - RequestTime: {RequestTimeMs}ms, UserId: {UserId}",
stopwatch.ElapsedMilliseconds, userId ?? "anonymous");
return StatusCode(500, new { error = "Erro interno do servidor", success = false });
return StatusCode(402, new {
success = false,
error = result.Message,
redirectUrl = "/Pagamento/SelecaoPlano"
});
}
return BadRequest(new { success = false, error = result.Message });
}
// Gerenciar Cookie de DeviceID para anônimos
if (!context.IsAuthenticated && string.IsNullOrEmpty(context.DeviceId))
{
var newDeviceId = Guid.NewGuid().ToString("N");
Response.Cookies.Append("_qr_device_id", newDeviceId, new CookieOptions
{
Expires = DateTime.UtcNow.AddYears(1),
HttpOnly = true,
Secure = true,
SameSite = SameSiteMode.Strict
});
}
return Ok(new
{
success = true,
QRCodeBase64 = result.QRCodeBase64,
QRId = result.QRId,
FromCache = result.FromCache,
RemainingQRs = context.IsAuthenticated ? result.RemainingCredits : (result.RemainingFreeQRs > 0 ? result.RemainingFreeQRs : 0),
Message = result.Message,
GenerationTimeMs = result.GenerationTimeMs
});
}
[HttpGet("GetUserStats")]
public async Task<IActionResult> GetUserStats()
{
var userId = User?.FindFirst(ClaimTypes.NameIdentifier)?.Value;
if (string.IsNullOrEmpty(userId)) return Unauthorized();
var user = await _userService.GetUserAsync(userId);
if (user == null) return NotFound();
return Ok(new
{
credits = user.Credits,
freeUsed = user.FreeQRsUsed,
freeLimit = 5,
isPremium = user.Credits > 0 || user.FreeQRsUsed < 5
});
}
[HttpGet("Download/{qrId}")]
public async Task<IActionResult> Download(string qrId, string format = "png")
{
var userId = User?.FindFirst(ClaimTypes.NameIdentifier)?.Value;
var stopwatch = Stopwatch.StartNew();
using (_logger.BeginScope(new Dictionary<string, object>
try
{
["QRId"] = qrId,
["Format"] = format.ToLower(),
["UserId"] = userId ?? "anonymous",
["QRDownload"] = true
}))
var qrData = await _userService.GetQRDataAsync(qrId);
if (qrData == null) return NotFound();
byte[] fileContent = Convert.FromBase64String(qrData.QRCodeBase64);
var contentType = "image/png";
var fileName = $"qrrapido-{qrId}.png";
if (format == "svg")
{
fileContent = await _qrService.ConvertToSvgAsync(qrData.QRCodeBase64);
contentType = "image/svg+xml";
fileName = fileName.Replace(".png", ".svg");
}
else if (format == "pdf")
{
fileContent = await _qrService.ConvertToPdfAsync(qrData.QRCodeBase64, qrData.Size);
contentType = "application/pdf";
fileName = fileName.Replace(".png", ".pdf");
}
return File(fileContent, contentType, fileName);
}
catch (Exception ex)
{
_logger.LogInformation("QR download requested - QRId: {QRId}, Format: {Format}", qrId, format);
try
{
var qrData = await _userService.GetQRDataAsync(qrId);
if (qrData == null)
{
_logger.LogWarning("QR download failed - QR code not found: {QRId}", qrId);
return NotFound();
}
var contentType = format.ToLower() switch
{
"svg" => "image/svg+xml",
"pdf" => "application/pdf",
_ => "image/png"
};
var fileName = $"qrrapido-{DateTime.Now:yyyyMMdd-HHmmss}.{format}";
_logger.LogDebug("Converting QR to format - QRId: {QRId}, Format: {Format}, Size: {Size}",
qrId, format, qrData.Size);
byte[] fileContent;
if (format.ToLower() == "svg")
{
fileContent = await _qrService.ConvertToSvgAsync(qrData.QRCodeBase64);
}
else if (format.ToLower() == "pdf")
{
fileContent = await _qrService.ConvertToPdfAsync(qrData.QRCodeBase64, qrData.Size);
}
else
{
fileContent = Convert.FromBase64String(qrData.QRCodeBase64);
}
stopwatch.Stop();
_logger.LogInformation("QR download completed - QRId: {QRId}, Format: {Format}, Size: {FileSize} bytes, ProcessingTime: {ProcessingTimeMs}ms",
qrId, format, fileContent.Length, stopwatch.ElapsedMilliseconds);
return File(fileContent, contentType, fileName);
}
catch (Exception ex)
{
stopwatch.Stop();
_logger.LogError(ex, "QR download failed - QRId: {QRId}, Format: {Format}, ProcessingTime: {ProcessingTimeMs}ms",
qrId, format, stopwatch.ElapsedMilliseconds);
return StatusCode(500);
}
_logger.LogError(ex, "Download error");
return StatusCode(500);
}
}
[HttpPost("SaveToHistory")]
public async Task<IActionResult> SaveToHistory([FromBody] SaveToHistoryRequest request)
[HttpGet("History")]
public async Task<IActionResult> GetHistory(int limit = 20)
{
var userId = User?.FindFirst(ClaimTypes.NameIdentifier)?.Value;
using (_logger.BeginScope(new Dictionary<string, object>
{
["QRId"] = request.QrId,
["UserId"] = userId ?? "anonymous",
["SaveToHistory"] = true
}))
{
_logger.LogInformation("Save to history requested - QRId: {QRId}", request.QrId);
try
{
if (string.IsNullOrEmpty(userId))
{
_logger.LogWarning("Save to history failed - user not authenticated");
return Unauthorized();
}
var qrData = await _userService.GetQRDataAsync(request.QrId);
if (qrData == null)
{
_logger.LogWarning("Save to history failed - QR code not found: {QRId}", request.QrId);
return NotFound();
}
// QR is already saved when generated, just return success
_logger.LogInformation("QR code already saved in history - QRId: {QRId}", request.QrId);
return Ok(new { success = true, message = "QR Code salvo no histórico!" });
}
catch (Exception ex)
{
_logger.LogError(ex, "Save to history failed - QRId: {QRId}", request.QrId);
return StatusCode(500, new { error = "Erro ao salvar no histórico." });
}
}
if (string.IsNullOrEmpty(userId)) return Unauthorized();
return Ok(await _userService.GetUserQRHistoryAsync(userId, limit));
}
[HttpPost("GenerateRapidWithLogo")]
public async Task<IActionResult> GenerateRapidWithLogo([FromForm] QRGenerationRequest request, IFormFile? logo)
{
var stopwatch = Stopwatch.StartNew();
var requestId = Guid.NewGuid().ToString("N")[..8];
var userId = User?.FindFirst(ClaimTypes.NameIdentifier)?.Value;
var isAuthenticated = User?.Identity?.IsAuthenticated ?? false;
using (_logger.BeginScope(new Dictionary<string, object>
{
["RequestId"] = requestId,
["UserId"] = userId ?? "anonymous",
["IsAuthenticated"] = isAuthenticated,
["QRType"] = request.Type ?? "unknown",
["ContentLength"] = request.Content?.Length ?? 0,
["QRGeneration"] = true,
["HasLogo"] = logo != null
}))
{
_logger.LogInformation("QR generation with logo request started - Type: {QRType}, ContentLength: {ContentLength}, HasLogo: {HasLogo}",
request.Type, request.Content?.Length ?? 0, logo != null);
try
{
// Quick validations
if (string.IsNullOrWhiteSpace(request.Content))
{
_logger.LogWarning("QR generation failed - empty content provided");
return BadRequest(new { error = "Conteúdo é obrigatório", success = false });
}
var userId = User?.FindFirst(ClaimTypes.NameIdentifier)?.Value;
if (string.IsNullOrEmpty(userId)) return Unauthorized();
if (request.Content.Length > 4000)
{
_logger.LogWarning("QR generation failed - content too long: {ContentLength} characters", request.Content.Length);
return BadRequest(new { error = "Conteúdo muito longo. Máximo 4000 caracteres.", success = false });
}
if (logo != null)
{
using var ms = new MemoryStream();
await logo.CopyToAsync(ms);
request.Logo = ms.ToArray();
request.HasLogo = true;
}
// Check user status
var user = await _userService.GetUserAsync(userId);
var context = new UserRequesterContext
{
UserId = userId,
IpAddress = HttpContext.Connection.RemoteIpAddress?.ToString() ?? "unknown"
};
// Validate premium status for logo feature
if (user?.IsPremium != true)
{
_logger.LogWarning("Logo upload attempted by non-premium user - UserId: {UserId}", userId ?? "anonymous");
return BadRequest(new
{
error = "Logo personalizado é exclusivo do plano Premium. Faça upgrade para usar esta funcionalidade.",
requiresPremium = true,
success = false
});
}
var result = await _qrBusinessManager.ProcessGenerationAsync(request, context);
// Validate premium corner styles
if (!string.IsNullOrEmpty(request.CornerStyle) && request.CornerStyle != "square")
{
_logger.LogInformation("Premium user using custom corner style - UserId: {UserId}, CornerStyle: {CornerStyle}",
userId, request.CornerStyle);
}
if (!result.Success) return BadRequest(result);
// Process logo upload if provided
if (logo != null && logo.Length > 0)
{
// Validate file size (2MB max)
if (logo.Length > 2 * 1024 * 1024)
{
_logger.LogWarning("Logo upload failed - file too large: {FileSize} bytes", logo.Length);
return BadRequest(new { error = "Logo muito grande. Máximo 2MB.", success = false });
}
// Validate file format
var allowedTypes = new[] { "image/png", "image/jpeg", "image/jpg" };
if (!allowedTypes.Contains(logo.ContentType?.ToLower()))
{
_logger.LogWarning("Logo upload failed - invalid format: {ContentType}", logo.ContentType);
return BadRequest(new { error = "Formato inválido. Use PNG ou JPG.", success = false });
}
try
{
// Convert file to byte array
using var memoryStream = new MemoryStream();
await logo.CopyToAsync(memoryStream);
request.Logo = memoryStream.ToArray();
request.HasLogo = true;
_logger.LogInformation("Logo processed successfully - Size: {LogoSize} bytes, Format: {ContentType}",
logo.Length, logo.ContentType);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error processing logo file");
return BadRequest(new { error = "Erro ao processar logo.", success = false });
}
}
// Rate limiting for free users (premium users get unlimited)
var rateLimitPassed = await CheckRateLimitAsync(userId, user);
if (!rateLimitPassed)
{
_logger.LogWarning("QR generation rate limited - User: {UserId}, IsPremium: {IsPremium}",
userId ?? "anonymous", user?.IsPremium ?? false);
return StatusCode(429, new
{
error = "Limite de QR codes atingido",
upgradeUrl = "/Premium/Upgrade",
success = false
});
}
// Configure optimizations based on user
request.IsPremium = user?.IsPremium == true;
request.OptimizeForSpeed = true;
_logger.LogDebug("Generating QR code with logo - IsPremium: {IsPremium}, HasLogo: {HasLogo}",
request.IsPremium, request.HasLogo);
// Generate QR code
var generationStopwatch = Stopwatch.StartNew();
var result = await _qrService.GenerateRapidAsync(request);
generationStopwatch.Stop();
if (!result.Success)
{
_logger.LogError("QR generation failed - Error: {ErrorMessage}, GenerationTime: {GenerationTimeMs}ms",
result.ErrorMessage, generationStopwatch.ElapsedMilliseconds);
return StatusCode(500, new { error = result.ErrorMessage, success = false });
}
_logger.LogInformation("QR code with logo generated successfully - GenerationTime: {GenerationTimeMs}ms, FromCache: {FromCache}, HasLogo: {HasLogo}",
generationStopwatch.ElapsedMilliseconds, result.FromCache, request.HasLogo);
// Save to history if user is logged in (fire and forget)
if (userId != null)
{
_ = Task.Run(async () =>
{
try
{
await _userService.SaveQRToHistoryAsync(userId, result);
_logger.LogDebug("QR code saved to history successfully for user {UserId}", userId);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error saving QR to history for user {UserId}", userId);
}
});
}
stopwatch.Stop();
return Ok(result);
}
catch (Exception ex)
{
stopwatch.Stop();
_logger.LogError(ex, "QR generation with logo failed with exception - RequestTime: {RequestTimeMs}ms, UserId: {UserId}",
stopwatch.ElapsedMilliseconds, userId ?? "anonymous");
return StatusCode(500, new { error = "Erro interno do servidor", success = false });
}
}
return Ok(new {
success = true,
QRCodeBase64 = result.QRCodeBase64,
QRId = result.QRId,
RemainingQRs = result.RemainingCredits
});
}
[HttpGet("History")]
public async Task<IActionResult> GetHistory(int limit = 20)
[HttpPost("SaveToHistory")]
public async Task<IActionResult> SaveToHistory([FromBody] SaveToHistoryRequest request)
{
try
{
var userId = User?.FindFirst(ClaimTypes.NameIdentifier)?.Value;
if (string.IsNullOrEmpty(userId))
{
return Unauthorized();
}
var history = await _userService.GetUserQRHistoryAsync(userId, limit);
return Ok(history);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error getting QR history");
return StatusCode(500);
}
}
private async Task<bool> CheckRateLimitAsync(string? userId, Models.User? user)
{
if (user?.IsPremium == true) return true;
var dailyLimit = userId != null ? 50 : 10; // Logged in: 50/day, Anonymous: 10/day
var currentCount = await _userService.GetDailyQRCountAsync(userId);
return currentCount < dailyLimit;
return Ok(new { success = true });
}
}
public class SaveToHistoryRequest
{
public string QrId { get; set; } = string.Empty;

View File

@ -0,0 +1,151 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Cors;
using Microsoft.AspNetCore.Mvc;
using QRRapidoApp.Filters;
using QRRapidoApp.Models.DTOs;
using QRRapidoApp.Models.ViewModels;
using QRRapidoApp.Services;
namespace QRRapidoApp.Controllers
{
/// <summary>
/// QRRapido public API v1 — generate QR codes programmatically.
/// Authenticate with your API key using the <c>X-API-Key</c> header.
/// Get your key at <c>https://qrrapido.site/Developer</c>.
/// </summary>
[ApiController]
[Route("api/v1/[controller]")]
[EnableCors("ApiPolicy")]
[ApiKeyAuthorize]
[Produces("application/json")]
public class QRManagerController : ControllerBase
{
private readonly IQRBusinessManager _qrBusinessManager;
private readonly ILogger<QRManagerController> _logger;
private static readonly HashSet<string> _validFormats = new(StringComparer.OrdinalIgnoreCase)
{ "png", "webp", "svg" };
private static readonly HashSet<string> _validTypes = new(StringComparer.OrdinalIgnoreCase)
{ "url", "pix", "wifi", "vcard", "whatsapp", "email", "sms", "texto" };
public QRManagerController(IQRBusinessManager qrBusinessManager, ILogger<QRManagerController> logger)
{
_qrBusinessManager = qrBusinessManager;
_logger = logger;
}
/// <summary>Health check — no API key required.</summary>
/// <response code="200">API is running.</response>
[HttpGet("ping")]
[AllowAnonymous]
[ProducesResponseType(StatusCodes.Status200OK)]
public IActionResult Ping() =>
Ok(new { status = "QRRapido API v1 is up", timestamp = DateTime.UtcNow });
/// <summary>
/// Generate a QR code and receive it as a base64-encoded image.
/// </summary>
/// <remarks>
/// Supported output formats: **png** (default), **webp** (recommended — ~40% smaller), **svg**.
///
/// Rate limits and monthly quotas are returned in response headers:
/// `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset`, `X-Quota-Limit`, `X-Quota-Remaining`.
/// </remarks>
/// <response code="200">QR code generated successfully.</response>
/// <response code="400">Invalid request parameters.</response>
/// <response code="401">Missing or invalid API key.</response>
/// <response code="402">Insufficient credits.</response>
/// <response code="429">Rate limit or monthly quota exceeded.</response>
/// <response code="500">Internal server error.</response>
[HttpPost("generate")]
[ProducesResponseType(typeof(QRResponseDto), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(object), StatusCodes.Status400BadRequest)]
[ProducesResponseType(typeof(object), StatusCodes.Status401Unauthorized)]
[ProducesResponseType(typeof(object), StatusCodes.Status402PaymentRequired)]
[ProducesResponseType(typeof(object), StatusCodes.Status429TooManyRequests)]
public async Task<IActionResult> Generate([FromBody] QRGenerationRequest request)
{
// Input validation
if (string.IsNullOrWhiteSpace(request.Content))
return BadRequest(new { error = "Field 'content' is required." });
if (request.Content.Length > 2048)
return BadRequest(new { error = "Field 'content' exceeds the maximum length of 2048 characters." });
var format = (request.OutputFormat ?? "png").ToLowerInvariant();
if (!_validFormats.Contains(format))
return BadRequest(new { error = $"Invalid 'outputFormat'. Allowed values: {string.Join(", ", _validFormats)}." });
var type = (request.Type ?? "url").ToLowerInvariant();
if (!_validTypes.Contains(type))
return BadRequest(new { error = $"Invalid 'type'. Allowed values: {string.Join(", ", _validTypes)}." });
request.OutputFormat = format;
request.Type = type;
var userId = HttpContext.Items["ApiKeyUserId"] as string;
var context = new UserRequesterContext
{
UserId = userId,
IpAddress = HttpContext.Connection.RemoteIpAddress?.ToString() ?? "api"
};
var result = await _qrBusinessManager.ProcessGenerationAsync(request, context);
if (!result.Success)
{
return result.ErrorCode switch
{
"INSUFFICIENT_CREDITS" => StatusCode(402, result),
_ => BadRequest(result)
};
}
// Map format and mimeType into the response
result.Format = format;
result.MimeType = format switch
{
"webp" => "image/webp",
"svg" => "image/svg+xml",
_ => "image/png"
};
return Ok(result);
}
/// <summary>
/// Smoke-test endpoint — generates a QR code without an API key (GET, for quick browser testing).
/// </summary>
[HttpGet("generate-get-test")]
[AllowAnonymous]
[ApiExplorerSettings(IgnoreApi = true)] // Hidden from Swagger docs
public async Task<IActionResult> GenerateGetTest(
string content = "https://qrrapido.site",
string color = "000000",
string format = "png")
{
var request = new QRGenerationRequest
{
Content = content,
PrimaryColor = "#" + color.Replace("#", ""),
Type = "url",
Size = 400,
OutputFormat = format
};
var context = new UserRequesterContext
{
UserId = "browser-test",
IpAddress = HttpContext.Connection.RemoteIpAddress?.ToString()
};
var result = await _qrBusinessManager.ProcessGenerationAsync(request, context);
if (!result.Success) return BadRequest(result.Message);
byte[] imageBytes = Convert.FromBase64String(result.QRCodeBase64!);
var mimeType = format == "webp" ? "image/webp" : "image/png";
return File(imageBytes, mimeType);
}
}
}

View File

@ -0,0 +1,120 @@
using Microsoft.AspNetCore.Mvc;
using QRRapidoApp.Data;
using QRRapidoApp.Models;
using System.Diagnostics;
using System.Security.Claims;
namespace QRRapidoApp.Controllers
{
/// <summary>
/// Controller for handling user ratings
/// </summary>
[Route("api/ratings")]
[ApiController]
public class RatingsController : ControllerBase
{
private readonly MongoDbContext _dbContext;
private readonly ILogger<RatingsController> _logger;
public RatingsController(MongoDbContext dbContext, ILogger<RatingsController> logger)
{
_dbContext = dbContext;
_logger = logger;
}
/// <summary>
/// Submit a new rating
/// </summary>
[HttpPost]
public async Task<IActionResult> SubmitRating([FromBody] RatingSubmissionDto submission)
{
var stopwatch = Stopwatch.StartNew();
using (_logger.BeginScope(new Dictionary<string, object>
{
["Rating"] = submission.Rating,
["HasComment"] = !string.IsNullOrWhiteSpace(submission.Comment)
}))
{
_logger.LogInformation("Rating submission - Rating: {Rating}, HasName: {HasName}, HasEmail: {HasEmail}, HasComment: {HasComment}",
submission.Rating, !string.IsNullOrWhiteSpace(submission.Name), !string.IsNullOrWhiteSpace(submission.Email), !string.IsNullOrWhiteSpace(submission.Comment));
try
{
// Validate rating value
if (submission.Rating < 1 || submission.Rating > 5)
{
_logger.LogWarning("Invalid rating value - Rating: {Rating}", submission.Rating);
return BadRequest(new { error = "Rating must be between 1 and 5" });
}
// Check MongoDB connection
if (!_dbContext.IsConnected || _dbContext.Ratings == null)
{
_logger.LogError("MongoDB not connected or Ratings collection unavailable");
return StatusCode(503, new { error = "Database unavailable" });
}
// Get user ID if authenticated
var userId = User?.FindFirst(ClaimTypes.NameIdentifier)?.Value;
// Get culture from request
var culture = System.Globalization.CultureInfo.CurrentUICulture.Name;
// Get IP address
var ipAddress = HttpContext.Connection.RemoteIpAddress?.ToString();
// Create rating object
var rating = new Rating
{
RatingValue = submission.Rating,
Name = submission.Name,
Email = submission.Email,
Comment = submission.Comment,
Url = submission.Url ?? string.Empty,
UserAgent = submission.UserAgent ?? string.Empty,
UserId = userId,
CreatedAt = DateTime.UtcNow,
IpAddress = ipAddress,
Culture = culture
};
// Save to MongoDB
await _dbContext.Ratings.InsertOneAsync(rating);
stopwatch.Stop();
_logger.LogInformation("Rating saved successfully - RatingId: {RatingId}, Rating: {Rating}, UserId: {UserId}, ProcessingTime: {ProcessingTimeMs}ms",
rating.Id, rating.RatingValue, userId ?? "anonymous", stopwatch.ElapsedMilliseconds);
return Ok(new
{
success = true,
message = "Thank you for your feedback!",
ratingId = rating.Id
});
}
catch (Exception ex)
{
stopwatch.Stop();
_logger.LogError(ex, "Error saving rating - ProcessingTime: {ProcessingTimeMs}ms",
stopwatch.ElapsedMilliseconds);
return StatusCode(500, new { error = "Error saving rating" });
}
}
}
}
/// <summary>
/// DTO for rating submission
/// </summary>
public class RatingSubmissionDto
{
public int Rating { get; set; }
public string? Name { get; set; }
public string? Email { get; set; }
public string? Comment { get; set; }
public string? Url { get; set; }
public string? UserAgent { get; set; }
}
}

View File

@ -0,0 +1,15 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace QRRapidoApp.Controllers
{
[Authorize]
public class SupportController : Controller
{
[HttpGet]
public IActionResult PremiumContact()
{
return View();
}
}
}

View File

@ -0,0 +1,138 @@
using Microsoft.AspNetCore.Mvc;
using QRRapidoApp.Services;
using System.Diagnostics;
namespace QRRapidoApp.Controllers
{
/// <summary>
/// Controller for tracking QR code scans (Premium feature)
/// Handles redirect URLs and analytics
/// </summary>
[Route("t")]
public class TrackingController : ControllerBase
{
private readonly IUserService _userService;
private readonly ILogger<TrackingController> _logger;
public TrackingController(IUserService userService, ILogger<TrackingController> logger)
{
_userService = userService;
_logger = logger;
}
/// <summary>
/// Track QR code scan and redirect to original URL
/// URL format: https://qrrapido.site/t/{trackingId}
/// </summary>
[HttpGet("{trackingId}")]
public async Task<IActionResult> TrackAndRedirect(string trackingId)
{
var stopwatch = Stopwatch.StartNew();
using (_logger.BeginScope(new Dictionary<string, object>
{
["TrackingId"] = trackingId,
["QRTracking"] = true
}))
{
_logger.LogInformation("QR tracking request - TrackingId: {TrackingId}", trackingId);
try
{
// Get QR code from database
var qr = await _userService.GetQRByTrackingIdAsync(trackingId);
if (qr == null)
{
_logger.LogWarning("QR code not found for tracking - TrackingId: {TrackingId}", trackingId);
return NotFound("QR code not found");
}
// Increment scan count and update last access (fire and forget for performance)
_ = Task.Run(async () =>
{
try
{
await _userService.IncrementQRScanCountAsync(trackingId);
_logger.LogDebug("QR scan count incremented - TrackingId: {TrackingId}, NewCount: {NewCount}",
trackingId, qr.ScanCount + 1);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error incrementing scan count - TrackingId: {TrackingId}", trackingId);
}
});
stopwatch.Stop();
_logger.LogInformation("QR redirect - TrackingId: {TrackingId}, Destination: {Destination}, ProcessingTime: {ProcessingTimeMs}ms",
trackingId, qr.Content, stopwatch.ElapsedMilliseconds);
// Redirect to original URL (HTTP 302 temporary redirect)
return Redirect(qr.Content);
}
catch (Exception ex)
{
stopwatch.Stop();
_logger.LogError(ex, "QR tracking failed - TrackingId: {TrackingId}, ProcessingTime: {ProcessingTimeMs}ms",
trackingId, stopwatch.ElapsedMilliseconds);
// Return error page instead of exposing exception details
return StatusCode(500, "Error processing QR code");
}
}
}
/// <summary>
/// Get analytics for a specific QR code (requires authentication)
/// </summary>
[HttpGet("analytics/{trackingId}")]
public async Task<IActionResult> GetAnalytics(string trackingId)
{
var userId = User?.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value;
if (string.IsNullOrEmpty(userId))
{
_logger.LogWarning("Analytics request without authentication - TrackingId: {TrackingId}", trackingId);
return Unauthorized();
}
try
{
var qr = await _userService.GetQRByTrackingIdAsync(trackingId);
if (qr == null)
{
_logger.LogWarning("Analytics request for non-existent QR - TrackingId: {TrackingId}", trackingId);
return NotFound();
}
// Verify ownership
if (qr.UserId != userId)
{
_logger.LogWarning("Analytics request for QR owned by different user - TrackingId: {TrackingId}, RequestingUser: {UserId}, Owner: {OwnerId}",
trackingId, userId, qr.UserId);
return Forbid();
}
_logger.LogInformation("Analytics retrieved - TrackingId: {TrackingId}, UserId: {UserId}, ScanCount: {ScanCount}",
trackingId, userId, qr.ScanCount);
return Ok(new
{
trackingId = qr.TrackingId,
scanCount = qr.ScanCount,
createdAt = qr.CreatedAt,
lastAccessedAt = qr.LastAccessedAt,
content = qr.Content,
type = qr.Type
});
}
catch (Exception ex)
{
_logger.LogError(ex, "Error retrieving analytics - TrackingId: {TrackingId}, UserId: {UserId}",
trackingId, userId);
return StatusCode(500);
}
}
}
}

View File

@ -0,0 +1,137 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Localization;
using QRRapidoApp.Services;
using System.Security.Claims;
namespace QRRapidoApp.Controllers
{
public class TutoriaisController : Controller
{
private readonly IMarkdownService _markdownService;
private readonly AdDisplayService _adDisplayService;
private readonly ILogger<TutoriaisController> _logger;
private readonly IStringLocalizer<QRRapidoApp.Resources.SharedResource> _localizer;
private readonly IConfiguration _config;
public TutoriaisController(
IMarkdownService markdownService,
AdDisplayService adDisplayService,
ILogger<TutoriaisController> logger,
IStringLocalizer<QRRapidoApp.Resources.SharedResource> localizer,
IConfiguration config)
{
_markdownService = markdownService;
_adDisplayService = adDisplayService;
_logger = logger;
_localizer = localizer;
_config = config;
}
// Portuguese: /tutoriais/{slug} (canonical, no prefix)
// Spanish: /es/tutoriais/{slug}
// English: /en/tutoriais/{slug}
[Route("tutoriais/{slug}")]
[Route("es/tutoriais/{slug}")]
[Route("en/tutoriais/{slug}")]
public async Task<IActionResult> Article(string slug, string? culture = null)
{
try
{
// Determine culture from URL path if not provided
var reqPath = Request.Path.Value ?? "";
culture ??= reqPath.StartsWith("/es/", StringComparison.OrdinalIgnoreCase) ? "es"
: reqPath.StartsWith("/en/", StringComparison.OrdinalIgnoreCase) ? "en"
: "pt-BR";
var article = await _markdownService.GetArticleAsync(slug, culture);
if (article == null)
{
_logger.LogWarning("Article not found: {Slug} ({Culture})", slug, culture);
return NotFound();
}
// Set ViewBag for ads and user info
var userId = User?.FindFirst(ClaimTypes.NameIdentifier)?.Value;
ViewBag.ShowAds = await _adDisplayService.ShouldShowAds(userId);
ViewBag.IsPremium = await _adDisplayService.HasValidPremiumSubscription(userId ?? "");
ViewBag.IsAuthenticated = User.Identity?.IsAuthenticated ?? false;
ViewBag.UserName = User.Identity?.Name ?? "";
_adDisplayService.SetViewBagAds(ViewBag);
// Set SEO metadata from article
ViewBag.Title = article.Metadata.Title;
ViewBag.Description = article.Metadata.Description;
ViewBag.Keywords = article.Metadata.Keywords;
ViewBag.OgImage = article.Metadata.Image;
ViewBag.OgType = "article";
ViewBag.ArticleAuthor = article.Metadata.Author;
ViewBag.ArticlePublishedTime = article.Metadata.Date.ToString("yyyy-MM-ddTHH:mm:ssZ");
ViewBag.ArticleModifiedTime = article.Metadata.LastMod.ToString("yyyy-MM-ddTHH:mm:ssZ");
ViewBag.Culture = culture;
ViewBag.Slug = slug;
// Get related articles (same culture, exclude current)
var allArticles = await _markdownService.GetAllArticlesAsync(culture);
article.RelatedArticles = allArticles
.Where(a => a.Title != article.Metadata.Title)
.Take(3)
.ToList();
_logger.LogInformation("Serving article: {Slug} ({Culture})", slug, culture);
return View(article);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error serving article: {Slug} ({Culture})", slug, culture);
return StatusCode(500, "Internal server error");
}
}
// Portuguese: /tutoriais (canonical, no prefix)
// Spanish: /es/tutoriais
// English: /en/tutoriais
[Route("tutoriais")]
[Route("es/tutoriais")]
[Route("en/tutoriais")]
public async Task<IActionResult> Index(string? culture = null)
{
try
{
// Determine culture from URL path if not provided
var reqPath = Request.Path.Value ?? "";
culture ??= reqPath.StartsWith("/es/", StringComparison.OrdinalIgnoreCase) ? "es"
: reqPath.StartsWith("/en/", StringComparison.OrdinalIgnoreCase) ? "en"
: "pt-BR";
var articles = await _markdownService.GetAllArticlesAsync(culture);
// Set ViewBag
var userId = User?.FindFirst(ClaimTypes.NameIdentifier)?.Value;
ViewBag.ShowAds = await _adDisplayService.ShouldShowAds(userId);
ViewBag.IsPremium = await _adDisplayService.HasValidPremiumSubscription(userId ?? "");
ViewBag.IsAuthenticated = User.Identity?.IsAuthenticated ?? false;
ViewBag.UserName = User.Identity?.Name ?? "";
_adDisplayService.SetViewBagAds(ViewBag);
ViewBag.Title = culture == "pt-BR" ? "Tutoriais QR Code"
: culture == "en" ? "QR Code Tutorials"
: "Tutoriales Código QR";
ViewBag.Description = culture == "pt-BR"
? "Aprenda a criar e usar QR Codes com nossos tutoriais completos"
: culture == "en"
? "Learn how to create and use QR Codes with our complete step-by-step tutorials"
: "Aprende a crear y usar códigos QR con nuestros tutoriales completos";
ViewBag.Culture = culture;
_logger.LogInformation("Serving tutorials index ({Culture}): {Count} articles", culture, articles.Count);
return View(articles);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error serving tutorials index ({Culture})", culture);
return StatusCode(500, "Internal server error");
}
}
}
}

195
DOCKER_SECRETS_SETUP.md Normal file
View File

@ -0,0 +1,195 @@
# Docker Secrets Setup - QR Rapido
Este documento descreve como configurar Docker Secrets para o QR Rapido em ambiente de produção.
## Por que usar Docker Secrets?
- Credenciais sensíveis não ficam expostas no código ou em arquivos de configuração
- Secrets são criptografados em repouso e em trânsito
- Apenas containers autorizados têm acesso
- Facilita rotação de credenciais sem rebuild de imagens
## Secrets Necessários
| Secret Name | Descrição | Exemplo |
|-------------|-----------|---------|
| `stripe_secret_key` | Stripe API Secret Key | `sk_live_xxx` |
| `stripe_webhook_secret` | Stripe Webhook Signing Secret | `whsec_xxx` |
| `mongodb_connection_string` | String de conexão MongoDB completa | `mongodb://user:pass@host:port/db?options` |
| `google_client_id` | Google OAuth Client ID | `xxx.apps.googleusercontent.com` |
| `google_client_secret` | Google OAuth Client Secret | `GOCSPX-xxx` |
| `microsoft_client_id` | Microsoft OAuth Client ID | `guid` |
| `microsoft_client_secret` | Microsoft OAuth Client Secret | `xxx` |
## Criando os Secrets (Uma única vez)
Conecte no servidor manager do Swarm e execute os comandos abaixo.
### Opção 1: Via linha de comando (menos seguro - fica no histórico)
```bash
# Stripe
echo "sk_live_SUBSTITUA_PELA_SUA_CHAVE" | docker secret create stripe_secret_key -
echo "whsec_SUBSTITUA_PELO_SEU_SECRET" | docker secret create stripe_webhook_secret -
# MongoDB
echo "mongodb://admin:SENHA@129.146.116.218:27017,141.148.162.114:27017/QrRapido?replicaSet=rs0&authSource=admin" | docker secret create mongodb_connection_string -
# Google OAuth
echo "SEU_GOOGLE_CLIENT_ID.apps.googleusercontent.com" | docker secret create google_client_id -
echo "GOCSPX-SEU_CLIENT_SECRET" | docker secret create google_client_secret -
# Microsoft OAuth
echo "SEU_MICROSOFT_CLIENT_ID" | docker secret create microsoft_client_id -
echo "SEU_MICROSOFT_CLIENT_SECRET" | docker secret create microsoft_client_secret -
```
### Opção 2: Via arquivo (mais seguro)
```bash
# Cria arquivo temporário, insere o valor, cria o secret, e remove o arquivo
nano /tmp/stripe_secret && docker secret create stripe_secret_key /tmp/stripe_secret && rm /tmp/stripe_secret
```
Repita para cada secret.
## Verificando Secrets Criados
```bash
# Lista todos os secrets
docker secret ls
# Deve mostrar:
# ID NAME CREATED
# xxx stripe_secret_key X minutes ago
# xxx stripe_webhook_secret X minutes ago
# xxx mongodb_connection_string X minutes ago
# xxx google_client_id X minutes ago
# xxx google_client_secret X minutes ago
# xxx microsoft_client_id X minutes ago
# xxx microsoft_client_secret X minutes ago
```
## Recriando o Service com Secrets
Se o service já existe sem os secrets, você precisa recriá-lo:
```bash
# Remove o service existente
docker service rm qrrapido-prod
# O próximo deploy vai criar o service com os secrets automaticamente
```
Ou manualmente:
```bash
docker service create \
--name qrrapido-prod \
--replicas 2 \
--network qrrapido-network \
--publish published=5001,target=8080 \
--mount type=bind,source=/app/keys,target=/app/keys \
--secret stripe_secret_key \
--secret stripe_webhook_secret \
--secret mongodb_connection_string \
--secret google_client_id \
--secret google_client_secret \
--secret microsoft_client_id \
--secret microsoft_client_secret \
--env ASPNETCORE_ENVIRONMENT=Production \
--env ASPNETCORE_URLS=http://+:8080 \
--update-delay 30s \
--update-parallelism 1 \
--update-order start-first \
--restart-condition on-failure \
--restart-max-attempts 3 \
registry.redecarneir.us/qrrapido:latest
```
## Atualizando um Secret
Docker Secrets são imutáveis. Para atualizar:
```bash
# 1. Cria novo secret com nome diferente
echo "novo_valor" | docker secret create stripe_secret_key_v2 -
# 2. Atualiza o service para usar o novo secret
docker service update \
--secret-rm stripe_secret_key \
--secret-add stripe_secret_key_v2 \
qrrapido-prod
# 3. Remove o secret antigo
docker secret rm stripe_secret_key
# 4. (Opcional) Renomeia o novo secret
# Infelizmente não é possível renomear, então mantenha a convenção de versões
# ou recrie o service com o nome correto
```
## Verificando se os Secrets estão funcionando
No rodapé da página, você verá:
- **QR Rapido v1.0.0 | Production ✓** - Secrets carregados corretamente
- **QR Rapido v1.0.0 | Production ✗** - Secrets NÃO carregados (problema!)
Você também pode verificar os logs:
```bash
docker service logs qrrapido-prod --tail 50 | grep -i secret
```
## Como funciona internamente
1. O Docker monta cada secret como arquivo em `/run/secrets/<nome_do_secret>`
2. O `DockerSecretsConfigurationProvider` lê esses arquivos na inicialização
3. Os valores sobrescrevem as configurações do `appsettings.json`
4. A variável `App:SecretsLoaded` é definida como `true` quando pelo menos um secret é carregado
## Testando Localmente
Para simular Docker Secrets localmente (sem Swarm):
```bash
# Cria diretório de secrets
mkdir -p /tmp/secrets
# Cria arquivos de teste
echo "sk_test_xxx" > /tmp/secrets/stripe_secret_key
echo "whsec_test" > /tmp/secrets/stripe_webhook_secret
# Executa o container com os secrets montados
docker run -d \
-v /tmp/secrets:/run/secrets:ro \
-p 5001:8080 \
-e ASPNETCORE_ENVIRONMENT=Development \
qrrapido:latest
```
## Troubleshooting
### Secret não está sendo lido
1. Verifique se o container tem acesso ao secret:
```bash
docker exec -it $(docker ps -q -f name=qrrapido) ls -la /run/secrets/
```
2. Verifique o conteúdo (apenas para debug):
```bash
docker exec -it $(docker ps -q -f name=qrrapido) cat /run/secrets/stripe_secret_key
```
### Service não inicia
Verifique se todos os secrets existem antes de criar o service:
```bash
docker secret ls
```
### Erro "secret not found"
O secret precisa existir ANTES de criar/atualizar o service que o usa.

View File

@ -11,6 +11,7 @@ namespace QRRapidoApp.Data
public MongoDbContext(IConfiguration configuration, IMongoClient? mongoClient = null)
{
var connectionString = configuration.GetConnectionString("MongoDB");
connectionString = connectionString + (connectionString.Contains("?") ? "&" : "?") + "maxPoolSize=200&minPoolSize=50&maxIdleTimeMS=30000";
if (mongoClient != null && !string.IsNullOrEmpty(connectionString))
{
try
@ -33,7 +34,9 @@ namespace QRRapidoApp.Data
public IMongoCollection<User> Users => _database.GetCollection<User>("users");
public IMongoCollection<QRCodeHistory> QRCodeHistory => _database.GetCollection<QRCodeHistory>("qrCodeHistory");
public IMongoCollection<Plan> Plans => _database.GetCollection<Plan>("plans");
public IMongoCollection<Order> Orders => _database.GetCollection<Order>("orders");
public IMongoCollection<AdFreeSession>? AdFreeSessions => _isConnected ? _database?.GetCollection<AdFreeSession>("ad_free_sessions") : null;
public IMongoCollection<Rating>? Ratings => _isConnected ? _database?.GetCollection<Rating>("ratings") : null;
public IMongoDatabase? Database => _isConnected ? _database : null;
public bool IsConnected => _isConnected;
@ -91,6 +94,16 @@ namespace QRRapidoApp.Data
)
)
});
// Rating indexes
var ratingIndexKeys = Builders<Rating>.IndexKeys;
await Ratings.Indexes.CreateManyAsync(new[]
{
new CreateIndexModel<Rating>(ratingIndexKeys.Descending(r => r.CreatedAt)),
new CreateIndexModel<Rating>(ratingIndexKeys.Ascending(r => r.UserId)),
new CreateIndexModel<Rating>(ratingIndexKeys.Ascending(r => r.RatingValue)),
new CreateIndexModel<Rating>(ratingIndexKeys.Ascending(r => r.Culture))
});
}
}
}

View File

@ -2,53 +2,42 @@
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /src
# Copy csproj and restore dependencies (better caching)
COPY ["QRRapidoApp.csproj", "./"]
RUN dotnet restore "QRRapidoApp.csproj"
# Install Node.js for frontend build
RUN curl -fsSL https://deb.nodesource.com/setup_18.x | bash - \
&& apt-get install -y nodejs
# Copy source code
# Copy csproj and restore as distinct layers
COPY *.csproj ./
RUN dotnet restore --runtime linux-arm64
# Copy everything else and build
COPY . .
# Build optimized for production
RUN dotnet build "QRRapidoApp.csproj" -c Release -o /app/build --no-restore
RUN dotnet build "QRRapidoApp.csproj" -c Release --runtime linux-arm64 --no-restore
# Publish stage
FROM build AS publish
RUN dotnet publish "QRRapidoApp.csproj" -c Release -o /app/publish --no-restore --no-build \
/p:PublishReadyToRun=true \
/p:PublishSingleFile=false \
/p:PublishTrimmed=false
RUN dotnet publish "QRRapidoApp.csproj" -c Release -o /app/publish \
--runtime linux-arm64 \
--no-restore \
--no-build \
--self-contained false
# Runtime stage
# Final stage/image
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS final
WORKDIR /app
# Install dependencies for QR code generation
RUN apt-get update && apt-get install -y \
libgdiplus \
libc6-dev \
curl \
&& rm -rf /var/lib/apt/lists/*
# Install libgdiplus for System.Drawing (se necess<73>rio para QR codes)
RUN apt-get update && apt-get install -y libgdiplus && rm -rf /var/lib/apt/lists/*
# Copy application
# Copy published app
COPY --from=publish /app/publish .
# Configure production environment
ENV ASPNETCORE_ENVIRONMENT=Production
ENV ASPNETCORE_URLS=http://+:80
ENV DOTNET_EnableDiagnostics=0
# Expose port
EXPOSE 8080
# Create non-root user for security
RUN addgroup --system --gid 1001 qrrapido
RUN adduser --system --uid 1001 qrrapido
# Create non-root user
RUN adduser --disabled-password --gecos '' appuser && chown -R appuser /app
USER appuser
# Set ownership
RUN chown -R qrrapido:qrrapido /app
USER qrrapido
# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD curl -f http://localhost/health || exit 1
EXPOSE 80
# Set entry point
ENTRYPOINT ["dotnet", "QRRapidoApp.dll"]

View File

@ -0,0 +1,345 @@
# 📊 QR Code Analytics - Premium Feature
Sistema de rastreamento de leituras de QR codes para usuários premium.
## 🎯 Visão Geral
Permite que usuários premium acompanhem quantas vezes seus QR codes foram escaneados, com timestamps e histórico completo.
### Como Funciona
1. **Usuário premium** gera QR code com analytics habilitado
2. QR code aponta para URL de redirect: `https://qrrapido.site/t/{trackingId}`
3. Ao escanear, o sistema:
- Incrementa contador de leituras (`scanCount`)
- Atualiza timestamp da última leitura (`lastAccessedAt`)
- Redireciona para URL original (HTTP 302)
---
## 🔧 Implementação Técnica
### Novos Componentes
#### 1. **TrackingController** (`Controllers/TrackingController.cs`)
Endpoints:
- `GET /t/{trackingId}` - Redireciona e contabiliza leitura
- `GET /t/analytics/{trackingId}` - Retorna analytics (requer autenticação)
#### 2. **Campos no MongoDB**
**QRCodeHistory** (campos já existentes, agora em uso):
```csharp
public int ScanCount { get; set; } = 0; // Contador de leituras
public DateTime LastAccessedAt { get; set; } // Última leitura
public bool IsDynamic { get; set; } = false; // Se é trackable
public string? TrackingId { get; set; } // ID público (novo)
```
#### 3. **QRGenerationRequest** (novo campo)
```csharp
public bool EnableTracking { get; set; } = false; // Premium feature
```
#### 4. **UserService** (novos métodos)
```csharp
Task<QRCodeHistory?> GetQRByTrackingIdAsync(string trackingId);
Task IncrementQRScanCountAsync(string trackingId);
```
---
## 🚀 Como Usar
### Backend (já implementado)
#### Gerar QR com Analytics
```csharp
var request = new QRGenerationRequest
{
Type = "url",
Content = "https://google.com",
IsPremium = true,
EnableTracking = true // ✅ Habilita analytics
};
var result = await _qrService.GenerateRapidAsync(request);
// result.TrackingId conterá o ID para analytics
// QR code aponta para: https://qrrapido.site/t/{trackingId}
```
#### Consultar Analytics
```csharp
// Via UserService
var qr = await _userService.GetQRByTrackingIdAsync("abc123def456");
Console.WriteLine($"Leituras: {qr.ScanCount}");
Console.WriteLine($"Última leitura: {qr.LastAccessedAt}");
// Via API (requer autenticação)
GET /t/analytics/abc123def456
```
**Response:**
```json
{
"trackingId": "abc123def456",
"scanCount": 42,
"createdAt": "2025-10-19T10:00:00Z",
"lastAccessedAt": "2025-10-20T15:30:00Z",
"content": "https://google.com",
"type": "url"
}
```
---
### Frontend (ainda não implementado)
#### Opção 1: Checkbox no Gerador
```html
<!-- Em Views/Home/Index.cshtml -->
<div class="form-check premium-feature">
<input type="checkbox" id="enableTracking" class="form-check-input">
<label for="enableTracking">
<i class="fas fa-chart-line"></i> Habilitar Analytics
<span class="badge bg-warning">Premium</span>
</label>
</div>
```
```javascript
// Em wwwroot/js/qr-speed-generator.js
const enableTracking = document.getElementById('enableTracking');
formData.append('EnableTracking', enableTracking?.checked && this.isPremium);
```
#### Opção 2: Dashboard de Analytics
```html
<!-- Nova view: Views/Premium/Analytics.cshtml -->
<div class="analytics-dashboard">
<h2>Meus QR Codes com Analytics</h2>
<div class="qr-list">
@foreach (var qr in Model.TrackableQRs)
{
<div class="qr-card">
<h3>@qr.Content</h3>
<p><strong>Leituras:</strong> @qr.ScanCount</p>
<p><strong>Última leitura:</strong> @qr.LastAccessedAt.ToString("dd/MM/yyyy HH:mm")</p>
<a href="/t/analytics/@qr.TrackingId">Ver detalhes</a>
</div>
}
</div>
</div>
```
---
## 📝 Limitações Conhecidas
### 1. **Apenas QR codes de URL**
- Tracking funciona apenas para `Type = "url"`
- WiFi, vCard, SMS, etc. **não suportam** redirect
**Razão**: Esses tipos não aceitam URLs customizadas
**Solução futura**: Implementar pixel de tracking invisível ou API de scan manual
### 2. **Redirect adiciona ~50-200ms**
- Usuário experimenta pequeno delay ao escanear
- Impacto: Mínimo para maioria dos casos
**Otimização**: Redirect é assíncrono, contador atualiza em background
### 3. **Bloqueadores de ads podem afetar**
- Alguns bloqueadores podem marcar `/t/` como tracking
- Probabilidade: Baixa (não usa cookies ou JS)
---
## 🔐 Segurança
### Proteções Implementadas
1. **Tracking ID não-sequencial**: GUID truncado (12 chars)
2. **Validação de ownership**: Endpoint `/t/analytics/` verifica se QR pertence ao usuário
3. **Fire-and-forget counting**: Não bloqueia redirect se MongoDB estiver lento
4. **Logging completo**: Todas as leituras são logadas
### Considerações de Privacidade
- **Não coleta**: IP, device, localização (por enquanto)
- **Apenas conta**: Quantidade de leituras + timestamp
- **LGPD compliant**: Usuário premium pode deletar histórico a qualquer momento
---
## 📊 Exemplos de Uso
### Caso 1: Rastreamento de Campanha
```csharp
// Gerar QR para campanha de marketing
var campaign = new QRGenerationRequest
{
Type = "url",
Content = "https://minhaloja.com/promo-verao",
IsPremium = true,
EnableTracking = true
};
var qr = await _qrService.GenerateRapidAsync(campaign);
// Distribuir QR em materiais impressos
// Depois consultar: await _userService.GetQRByTrackingIdAsync(qr.TrackingId);
```
### Caso 2: Validação de Engajamento
```csharp
// Verificar se QR code em outdoor teve leituras
var qr = await _userService.GetQRByTrackingIdAsync("abc123def456");
if (qr.ScanCount > 100)
{
Console.WriteLine("Outdoor teve boa visibilidade!");
}
else if (qr.ScanCount == 0)
{
Console.WriteLine("QR pode estar ilegível ou mal posicionado");
}
```
---
## 🛠️ Configuração
### appsettings.json
```json
{
"App": {
"BaseUrl": "https://qrrapido.site" // ✅ Usado para gerar URLs de tracking
}
}
```
### appsettings.Development.json
```json
{
"App": {
"BaseUrl": "https://localhost:52428" // ✅ Para testes locais
}
}
```
---
## 🧪 Testando Localmente
### 1. Gerar QR com Tracking
```bash
# POST /api/QR/GenerateRapid
curl -X POST https://localhost:52428/api/QR/GenerateRapid \
-H "Content-Type: application/json" \
-d '{
"type": "url",
"content": "https://google.com",
"isPremium": true,
"enableTracking": true
}'
```
**Response:**
```json
{
"qrCodeBase64": "iVBORw0KG...",
"trackingId": "a1b2c3d4e5f6",
"success": true
}
```
### 2. Simular Leitura do QR
```bash
# Abrir no navegador (ou curl)
curl -L https://localhost:52428/t/a1b2c3d4e5f6
# Deve redirecionar para https://google.com
```
### 3. Verificar Contador
```bash
# Acessar MongoDB ou via API
curl https://localhost:52428/t/analytics/a1b2c3d4e5f6 \
-H "Authorization: Bearer {token}"
```
**Response:**
```json
{
"trackingId": "a1b2c3d4e5f6",
"scanCount": 1,
"lastAccessedAt": "2025-10-20T10:00:00Z"
}
```
---
## 📈 Próximos Passos (Features Futuras)
### Analytics Avançado
- [ ] Gráfico de leituras por dia/semana/mês
- [ ] Geo-localização (IP → País/Cidade)
- [ ] Tipo de device (mobile/desktop)
- [ ] Browser/OS usado para escanear
### UI/UX
- [ ] Dashboard de analytics no painel premium
- [ ] Exportar dados para CSV/Excel
- [ ] Notificações quando QR atingir X leituras
### Performance
- [ ] Cache Redis para evitar queries MongoDB em cada scan
- [ ] Batch updates (atualizar contador a cada N leituras)
---
## 📚 Referências
- **Tracking URLs**: Mesma estratégia usada por bit.ly, tinyurl, etc.
- **HTTP 302 Redirect**: Padrão para preservar SEO e funcionalidade
- **Fire-and-forget**: Pattern do ASP.NET Core para operações assíncronas não-bloqueantes
---
## 🐛 Troubleshooting
### Problema: "QR code not found"
- Verificar se `trackingId` existe no MongoDB
- Verificar se QR foi salvo com `IsDynamic = true`
### Problema: Contador não incrementa
- Verificar logs do `TrackingController`
- Verificar conectividade MongoDB
- Verificar se `IncrementQRScanCountAsync` está sendo chamado
### Problema: Redirect não funciona
- Verificar se URL original está salva em `Content`
- Verificar se `BaseUrl` está configurado corretamente
- Verificar logs de erro no controller
---
**Implementado em**: 2025-10-20
**Versão**: 1.0.0
**Status**: ✅ Backend completo, Frontend pendente

View File

@ -0,0 +1,172 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using QRRapidoApp.Models;
using QRRapidoApp.Services;
namespace QRRapidoApp.Filters
{
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class ApiKeyAuthorizeAttribute : Attribute, IAsyncActionFilter
{
private const string ApiKeyHeaderName = "X-API-Key";
// Tracks 429 events per key for abuse logging (key: prefix, value: list of timestamps)
// In-process only; acceptable for the abuse detection use case.
private static readonly System.Collections.Concurrent.ConcurrentDictionary<string, System.Collections.Generic.Queue<DateTime>> _abuseTracker = new();
public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
{
// [AllowAnonymous] on the action bypasses this filter
var hasAllowAnonymous = context.ActionDescriptor.EndpointMetadata
.OfType<Microsoft.AspNetCore.Authorization.AllowAnonymousAttribute>()
.Any();
if (hasAllowAnonymous)
{
await next();
return;
}
var logger = context.HttpContext.RequestServices.GetRequiredService<ILogger<ApiKeyAuthorizeAttribute>>();
try
{
if (!context.HttpContext.Request.Headers.TryGetValue(ApiKeyHeaderName, out var extractedApiKey))
{
logger.LogWarning("API Key missing in request headers from {IP}", GetIp(context));
context.Result = JsonError(401, "API Key not provided. Use the X-API-Key header.");
return;
}
var userService = context.HttpContext.RequestServices.GetRequiredService<IUserService>();
var user = await userService.GetUserByApiKeyAsync(extractedApiKey!);
if (user == null)
{
var masked = MaskKey(extractedApiKey.ToString());
logger.LogWarning("Invalid API Key {Masked} from {IP}", masked, GetIp(context));
context.Result = JsonError(401, "Unauthorized. Invalid or revoked API Key.");
return;
}
// Find the matching ApiKeyConfig by prefix (first 8 chars of raw key)
var rawKey = extractedApiKey.ToString();
var prefix = rawKey.Length >= 8 ? rawKey[..8] : rawKey;
var apiKeyConfig = user.ApiKeys.FirstOrDefault(k => k.Prefix == prefix && k.IsActive);
if (apiKeyConfig == null)
{
context.Result = JsonError(403, "API Key is revoked or not found.");
return;
}
// Plan tier comes from the user's active API subscription, not from the key config.
// This ensures that upgrading/downgrading immediately affects all keys.
var effectiveTier = user.ApiSubscription?.EffectiveTier ?? ApiPlanTier.Free;
// Rate limit check
var rateLimitService = context.HttpContext.RequestServices.GetRequiredService<IApiRateLimitService>();
var rl = await rateLimitService.CheckAndIncrementAsync(prefix, effectiveTier);
// Always inject rate limit headers
AddRateLimitHeaders(context.HttpContext, rl);
if (!rl.Allowed)
{
var ip = GetIp(context);
TrackAbuse(prefix, logger, ip);
var message = rl.MonthlyExceeded
? $"Monthly quota exceeded ({rl.MonthlyLimit} requests). Upgrade your plan at qrrapido.site/Developer."
: $"Rate limit exceeded ({rl.PerMinuteLimit} req/min). Retry after {rl.ResetUnixTimestamp - DateTimeOffset.UtcNow.ToUnixTimeSeconds()}s.";
context.HttpContext.Response.Headers["Retry-After"] =
(rl.ResetUnixTimestamp - DateTimeOffset.UtcNow.ToUnixTimeSeconds()).ToString();
context.Result = JsonError(429, message, new
{
plan = ApiPlanLimits.PlanName(apiKeyConfig.PlanTier),
upgradeUrl = "https://qrrapido.site/Developer"
});
return;
}
context.HttpContext.Items["ApiKeyUserId"] = user.Id;
context.HttpContext.Items["ApiKeyPrefix"] = prefix;
context.HttpContext.Items["ApiPlanTier"] = effectiveTier;
await next();
}
catch (Exception ex)
{
logger.LogError(ex, "Error during API Key authorization.");
context.Result = JsonError(500, "Internal server error during authorization.");
}
}
// ── helpers ──────────────────────────────────────────────────────────
private static void AddRateLimitHeaders(HttpContext ctx, RateLimitResult rl)
{
if (rl.PerMinuteLimit >= 0)
{
ctx.Response.Headers["X-RateLimit-Limit"] = rl.PerMinuteLimit.ToString();
ctx.Response.Headers["X-RateLimit-Remaining"] = Math.Max(0, rl.PerMinuteLimit - rl.PerMinuteUsed).ToString();
}
else
{
ctx.Response.Headers["X-RateLimit-Limit"] = "unlimited";
ctx.Response.Headers["X-RateLimit-Remaining"] = "unlimited";
}
ctx.Response.Headers["X-RateLimit-Reset"] = rl.ResetUnixTimestamp.ToString();
if (rl.MonthlyLimit >= 0)
{
ctx.Response.Headers["X-Quota-Limit"] = rl.MonthlyLimit.ToString();
ctx.Response.Headers["X-Quota-Remaining"] = Math.Max(0, rl.MonthlyLimit - rl.MonthlyUsed).ToString();
}
else
{
ctx.Response.Headers["X-Quota-Limit"] = "unlimited";
ctx.Response.Headers["X-Quota-Remaining"] = "unlimited";
}
}
private static ObjectResult JsonError(int status, string message, object? extra = null)
{
object body = extra == null
? new { error = message }
: new { error = message, details = extra };
return new ObjectResult(body) { StatusCode = status };
}
private static string GetIp(ActionExecutingContext ctx) =>
ctx.HttpContext.Connection.RemoteIpAddress?.ToString() ?? "unknown";
private static string MaskKey(string key) =>
key.Length > 8 ? key[..8] + "..." : "***";
/// <summary>Logs a Warning when the same key hits 429 three times within 60 seconds.</summary>
private static void TrackAbuse(string prefix, ILogger logger, string ip)
{
var queue = _abuseTracker.GetOrAdd(prefix, _ => new System.Collections.Generic.Queue<DateTime>());
lock (queue)
{
var cutoff = DateTime.UtcNow.AddSeconds(-60);
while (queue.Count > 0 && queue.Peek() < cutoff)
queue.Dequeue();
queue.Enqueue(DateTime.UtcNow);
if (queue.Count >= 3)
{
logger.LogWarning(
"Potential abuse: API key prefix {Prefix} received {Count} rate-limit rejections in the last 60s. Client IP: {IP}",
prefix, queue.Count, ip);
}
}
}
}
}

View File

@ -0,0 +1,50 @@
namespace QRRapidoApp.Middleware
{
/// <summary>
/// Adds security headers and JSON error handling for /api/v1/* routes.
/// </summary>
public class ApiSecurityHeadersMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<ApiSecurityHeadersMiddleware> _logger;
public ApiSecurityHeadersMiddleware(RequestDelegate next, ILogger<ApiSecurityHeadersMiddleware> logger)
{
_next = next;
_logger = logger;
}
public async Task InvokeAsync(HttpContext context)
{
if (!context.Request.Path.StartsWithSegments("/api/v1"))
{
await _next(context);
return;
}
// Security headers for API responses
context.Response.Headers["X-Content-Type-Options"] = "nosniff";
context.Response.Headers["X-Frame-Options"] = "DENY";
try
{
await _next(context);
}
catch (Exception ex)
{
_logger.LogError(ex, "Unhandled exception on API route {Path}", context.Request.Path);
if (!context.Response.HasStarted)
{
context.Response.StatusCode = 500;
context.Response.ContentType = "application/json";
await context.Response.WriteAsJsonAsync(new
{
error = "An internal error occurred. Please try again later.",
requestId = context.TraceIdentifier
});
}
}
}
}
}

View File

@ -1,13 +1,25 @@
using System.Globalization;
using System.Linq;
namespace QRRapidoApp.Middleware
{
/// <summary>
/// Middleware de redirecionamento de idioma otimizado para SEO.
///
/// Comportamento:
/// - "/" → Retorna 200 OK em Português (canonical)
/// - "/pt-BR" ou "/pt-BR/*" → Redireciona 301 para "/" ou "/*" (sem prefixo)
/// - "/es" ou "/es/*" → Retorna 200 OK em Espanhol (mantém URL)
/// - "/en" ou "/en/*" → Retorna 200 OK em Inglês (mantém URL)
/// - "/pix", "/wifi", etc. → Retorna 200 OK em Português (sem redirect)
/// </summary>
public class LanguageRedirectionMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<LanguageRedirectionMiddleware> _logger;
private readonly string[] _supportedCultures = { "pt-BR", "es", "en" };
private const string DefaultCulture = "pt-BR";
private const string CookieName = ".AspNetCore.Culture";
public LanguageRedirectionMiddleware(RequestDelegate next, ILogger<LanguageRedirectionMiddleware> logger)
{
@ -17,97 +29,146 @@ namespace QRRapidoApp.Middleware
public async Task InvokeAsync(HttpContext context)
{
var path = context.Request.Path.Value?.TrimStart('/') ?? "";
// Skip if already has culture in path, or if it's an API, static file, or special route
if (HasCultureInPath(path) || IsSpecialRoute(path))
var pathString = context.Request.Path.Value ?? "";
// Skip API routes immediately using the raw path
if (pathString.StartsWith("/api", StringComparison.OrdinalIgnoreCase))
{
await _next(context);
return;
}
// Detect browser language
var detectedCulture = DetectBrowserLanguage(context);
// Build redirect URL with culture
var redirectUrl = $"/{detectedCulture}";
if (!string.IsNullOrEmpty(path))
// Skip redirection for non-GET requests (POST/PUT/PATCH/DELETE).
// A 301 redirect converts POST to GET, breaking form submissions.
// Language canonicalization only matters for crawlers (GET).
if (!HttpMethods.IsGet(context.Request.Method) && !HttpMethods.IsHead(context.Request.Method))
{
redirectUrl += $"/{path}";
}
// Add query string if present
if (context.Request.QueryString.HasValue)
{
redirectUrl += context.Request.QueryString.Value;
await _next(context);
return;
}
_logger.LogInformation("Redirecting to localized URL: {RedirectUrl} (detected culture: {Culture})",
redirectUrl, detectedCulture);
var path = pathString.TrimStart('/') ?? "";
context.Response.Redirect(redirectUrl, permanent: false);
}
private bool HasCultureInPath(string path)
{
if (string.IsNullOrEmpty(path))
return false;
// Skip other special routes
if (IsSpecialRoute(path))
{
await _next(context);
return;
}
var segments = path.Split('/', StringSplitOptions.RemoveEmptyEntries);
if (segments.Length == 0)
return false;
var firstSegment = segments.Length > 0 ? segments[0] : "";
return _supportedCultures.Contains(segments[0]);
// Check if URL starts with a culture segment (supported or unsupported)
if (IsCultureLikeSegment(firstSegment))
{
// Supported: pt-BR (Default/Canonical)
if (string.Equals(firstSegment, "pt-BR", StringComparison.OrdinalIgnoreCase))
{
RedirectToCanonical(context, segments);
return;
}
// Supported: es (Spanish neutral)
if (string.Equals(firstSegment, "es", StringComparison.OrdinalIgnoreCase))
{
SetCulture(context, "es");
await _next(context);
return;
}
// Supported: en (English)
if (string.Equals(firstSegment, "en", StringComparison.OrdinalIgnoreCase))
{
SetCulture(context, "en");
await _next(context);
return;
}
// Handle aliases or unsupported cultures (e.g. /en-US/, /pt/)
if (TryHandleCultureAliasOrUnknown(context, firstSegment, segments))
{
return;
}
}
// No culture prefix or unknown segment -> Serve in Portuguese (default)
SetCulture(context, DefaultCulture);
await _next(context);
}
private bool IsCultureLikeSegment(string segment)
{
if (string.IsNullOrEmpty(segment)) return false;
// Matches xx (like en, pt, es) or xx-XX (like pt-BR, en-US)
return segment.Length == 2 || (segment.Length == 5 && segment[2] == '-');
}
private bool TryHandleCultureAliasOrUnknown(HttpContext context, string firstSegment, string[] segments)
{
// Map known aliases to canonical forms
if (string.Equals(firstSegment, "es-py", StringComparison.OrdinalIgnoreCase))
return RedirectToLanguage(context, "es", segments);
if (string.Equals(firstSegment, "en-us", StringComparison.OrdinalIgnoreCase) ||
string.Equals(firstSegment, "en-gb", StringComparison.OrdinalIgnoreCase))
return RedirectToLanguage(context, "en", segments);
// For anything else that looks like a culture (pt, fr, de, etc.)
// redirect to the canonical Portuguese URL (no prefix).
return RedirectToCanonical(context, segments);
}
private bool RedirectToLanguage(HttpContext context, string culture, string[] segments)
{
var remainingPath = segments.Length > 1
? "/" + string.Join('/', segments.Skip(1))
: "";
var redirectUrl = "/" + culture + remainingPath;
return ExecuteRedirect(context, redirectUrl);
}
private bool RedirectToCanonical(HttpContext context, string[] segments)
{
var remainingPath = segments.Length > 1
? "/" + string.Join('/', segments.Skip(1))
: "/";
return ExecuteRedirect(context, remainingPath);
}
private bool ExecuteRedirect(HttpContext context, string url)
{
if (context.Request.QueryString.HasValue)
{
url += context.Request.QueryString.Value;
}
_logger.LogInformation("SEO Redirect: {Source} -> {Dest}", context.Request.Path, url);
context.Response.Redirect(url, permanent: true);
return true;
}
private void SetCulture(HttpContext context, string culture)
{
var cultureInfo = new CultureInfo(culture);
CultureInfo.CurrentCulture = cultureInfo;
CultureInfo.CurrentUICulture = cultureInfo;
context.Items["Culture"] = culture;
}
private bool IsSpecialRoute(string path)
{
var specialRoutes = new[]
{
"api/", "health", "_framework/", "lib/", "css/", "js/", "images/",
"favicon.ico", "robots.txt", "sitemap.xml",
"signin-microsoft", "signin-google", "signout-callback-oidc",
"Account/ExternalLoginCallback", "Account/Logout"
"api/", "health", "_framework/", "lib/", "css/", "js/", "images/",
"favicon.ico", "robots.txt", "sitemap.xml", "ads.txt",
"signin-", "signout-", "Account/", "Pagamento/", "Home/Error"
};
return specialRoutes.Any(route => path.StartsWith(route, StringComparison.OrdinalIgnoreCase));
}
private string DetectBrowserLanguage(HttpContext context)
{
var acceptLanguage = context.Request.GetTypedHeaders().AcceptLanguage;
if (acceptLanguage != null && acceptLanguage.Any())
{
// Check for exact matches first
foreach (var lang in acceptLanguage.OrderByDescending(x => x.Quality ?? 1.0))
{
var langCode = lang.Value.Value;
// Special case: es-PY should redirect to pt-BR
if (string.Equals(langCode, "es-PY", StringComparison.OrdinalIgnoreCase))
{
return DefaultCulture;
}
// Check exact match
if (_supportedCultures.Contains(langCode))
{
return langCode;
}
// Check language part only (e.g., 'es' from 'es-AR')
var languagePart = langCode.Split('-')[0];
var matchingCulture = _supportedCultures.FirstOrDefault(c => c.StartsWith(languagePart + "-") || c == languagePart);
if (matchingCulture != null)
{
return matchingCulture;
}
}
}
// Default fallback
return DefaultCulture;
}
}
}
}

View File

@ -0,0 +1,179 @@
using System;
using System.Collections.Generic;
namespace QRRapidoApp.Models.Ads
{
/// <summary>
/// Represents the content for a single affiliate advertisement.
/// </summary>
public class AffiliateAdContent
{
public string Title { get; set; } = string.Empty;
public string? Description { get; set; }
public string ProductUrl { get; set; } = string.Empty;
public string? ImageUrl { get; set; }
public string? CtaText { get; set; }
public string? BadgeText { get; set; }
public string? PriceText { get; set; }
public string? Category { get; set; }
public bool IsEmpty()
{
return string.IsNullOrWhiteSpace(ProductUrl);
}
public AffiliateAdContent Clone()
{
return new AffiliateAdContent
{
Title = Title,
Description = Description,
ProductUrl = ProductUrl,
ImageUrl = ImageUrl,
CtaText = CtaText,
BadgeText = BadgeText,
PriceText = PriceText,
Category = Category
};
}
}
/// <summary>
/// Configuration for an ad slot, supporting AdSense or affiliate content.
/// </summary>
public class AdSlotConfiguration
{
/// <summary>
/// Provider type for the slot. Supported values: "AdSense", "Affiliate".
/// Defaults to "AdSense" to preserve current behaviour.
/// </summary>
public string Provider { get; set; } = "AdSense";
/// <summary>
/// Optional custom AdSense slot ID override.
/// </summary>
public string? AdSenseSlotId { get; set; }
/// <summary>
/// Affiliate content that will be rendered when <see cref="Provider"/> is "Affiliate".
/// </summary>
public AffiliateAdContent? Affiliate { get; set; }
public AdSlotConfiguration Clone()
{
return new AdSlotConfiguration
{
Provider = Provider,
AdSenseSlotId = AdSenseSlotId,
Affiliate = Affiliate?.Clone()
};
}
}
/// <summary>
/// Root options object bound from configuration for ad slots.
/// </summary>
public class AdsConfigurationOptions
{
private IDictionary<string, AdSlotConfiguration> _slots = new Dictionary<string, AdSlotConfiguration>(StringComparer.OrdinalIgnoreCase);
private IDictionary<string, IDictionary<string, AdSlotConfiguration>> _locales = new Dictionary<string, IDictionary<string, AdSlotConfiguration>>(StringComparer.OrdinalIgnoreCase);
public IDictionary<string, AdSlotConfiguration> Slots
{
get => _slots;
set => _slots = CreateSlotDictionary(value);
}
public IDictionary<string, IDictionary<string, AdSlotConfiguration>> Locales
{
get => _locales;
set
{
_locales = new Dictionary<string, IDictionary<string, AdSlotConfiguration>>(StringComparer.OrdinalIgnoreCase);
if (value == null)
{
return;
}
foreach (var locale in value)
{
_locales[locale.Key] = CreateSlotDictionary(locale.Value);
}
}
}
public AdSlotConfiguration GetSlot(string slotKey, string? cultureName = null)
{
if (string.IsNullOrWhiteSpace(slotKey))
{
slotKey = "header";
}
var localeSlot = TryGetLocaleSlot(slotKey, cultureName);
if (localeSlot != null)
{
return localeSlot.Clone();
}
if (_slots.TryGetValue(slotKey, out var config))
{
return config.Clone();
}
// Default to AdSense when not configured
return new AdSlotConfiguration();
}
private AdSlotConfiguration? TryGetLocaleSlot(string slotKey, string? cultureName)
{
if (string.IsNullOrWhiteSpace(cultureName) || _locales.Count == 0)
{
return null;
}
if (_locales.TryGetValue(cultureName, out var localeSlots) && localeSlots.TryGetValue(slotKey, out var slot))
{
return slot;
}
var neutralCulture = cultureName.Split('-')[0];
if (!string.Equals(neutralCulture, cultureName, StringComparison.OrdinalIgnoreCase)
&& _locales.TryGetValue(neutralCulture, out var neutralSlots)
&& neutralSlots.TryGetValue(slotKey, out var neutralSlot))
{
return neutralSlot;
}
return null;
}
private static IDictionary<string, AdSlotConfiguration> CreateSlotDictionary(IDictionary<string, AdSlotConfiguration>? source)
{
var dictionary = new Dictionary<string, AdSlotConfiguration>(StringComparer.OrdinalIgnoreCase);
if (source == null)
{
return dictionary;
}
foreach (var kvp in source)
{
if (kvp.Value != null)
{
dictionary[kvp.Key] = kvp.Value;
}
}
return dictionary;
}
}
/// <summary>
/// View model used by the affiliate ad partial.
/// </summary>
public class AffiliateAdViewModel
{
public string SlotKey { get; set; } = "header";
public string ContainerCssClass { get; set; } = string.Empty;
public AffiliateAdContent Content { get; set; } = new AffiliateAdContent();
}
}

36
Models/ApiPlanTier.cs Normal file
View File

@ -0,0 +1,36 @@
namespace QRRapidoApp.Models
{
public enum ApiPlanTier
{
Free = 0,
Starter = 1,
Pro = 2,
Business = 3,
Enterprise = 4
}
public static class ApiPlanLimits
{
private static readonly Dictionary<ApiPlanTier, (int PerMinute, int PerMonth)> _limits = new()
{
{ ApiPlanTier.Free, (10, 500) },
{ ApiPlanTier.Starter, (50, 10_000) },
{ ApiPlanTier.Pro, (200, 100_000) },
{ ApiPlanTier.Business, (500, 500_000) },
{ ApiPlanTier.Enterprise, (int.MaxValue, int.MaxValue) },
};
public static (int PerMinute, int PerMonth) GetLimits(ApiPlanTier tier) =>
_limits.TryGetValue(tier, out var l) ? l : _limits[ApiPlanTier.Free];
public static string PlanName(ApiPlanTier tier) => tier switch
{
ApiPlanTier.Free => "Free",
ApiPlanTier.Starter => "Starter",
ApiPlanTier.Pro => "Pro",
ApiPlanTier.Business => "Business",
ApiPlanTier.Enterprise => "Enterprise",
_ => "Free"
};
}
}

36
Models/ApiSubscription.cs Normal file
View File

@ -0,0 +1,36 @@
using MongoDB.Bson.Serialization.Attributes;
namespace QRRapidoApp.Models
{
/// <summary>
/// Embedded document tracking the user's API subscription (separate from the QR credits plan).
/// </summary>
public class ApiSubscription
{
[BsonElement("tier")]
public ApiPlanTier Tier { get; set; } = ApiPlanTier.Free;
/// <summary>"free" | "active" | "past_due" | "canceled"</summary>
[BsonElement("status")]
public string Status { get; set; } = "free";
[BsonElement("stripeSubscriptionId")]
public string? StripeSubscriptionId { get; set; }
[BsonElement("stripeCustomerId")]
public string? StripeCustomerId { get; set; }
[BsonElement("currentPeriodEnd")]
public DateTime? CurrentPeriodEnd { get; set; }
[BsonElement("activatedAt")]
public DateTime? ActivatedAt { get; set; }
[BsonElement("canceledAt")]
public DateTime? CanceledAt { get; set; }
public bool IsActive => Status == "active" && (CurrentPeriodEnd == null || CurrentPeriodEnd > DateTime.UtcNow);
public ApiPlanTier EffectiveTier => IsActive ? Tier : ApiPlanTier.Free;
}
}

16
Models/ArticleMetadata.cs Normal file
View File

@ -0,0 +1,16 @@
namespace QRRapidoApp.Models
{
public class ArticleMetadata
{
public string Title { get; set; } = string.Empty;
public string Description { get; set; } = string.Empty;
public string Keywords { get; set; } = string.Empty;
public string Author { get; set; } = "QR Rapido";
public DateTime Date { get; set; }
public DateTime LastMod { get; set; }
public string Image { get; set; } = string.Empty;
public string Culture { get; set; } = "pt-BR";
public int ReadingTimeMinutes { get; set; }
public string Slug { get; set; } = string.Empty;
}
}

View File

@ -0,0 +1,22 @@
namespace QRRapidoApp.Models.DTOs
{
public class QRResponseDto
{
public bool Success { get; set; }
public string? QRCodeBase64 { get; set; }
public string? QRId { get; set; }
public string? Message { get; set; }
public string? ErrorCode { get; set; }
public int RemainingCredits { get; set; }
public int RemainingFreeQRs { get; set; }
public bool FromCache { get; set; }
public string? NewDeviceId { get; set; }
public long GenerationTimeMs { get; set; }
/// <summary>Output image format: "png", "webp" or "svg".</summary>
public string Format { get; set; } = "png";
/// <summary>MIME type of the encoded image (e.g. "image/png", "image/webp").</summary>
public string MimeType { get; set; } = "image/png";
}
}

View File

@ -0,0 +1,10 @@
namespace QRRapidoApp.Models.DTOs
{
public class UserRequesterContext
{
public string? UserId { get; set; }
public string? IpAddress { get; set; }
public string? DeviceId { get; set; }
public bool IsAuthenticated => !string.IsNullOrEmpty(UserId);
}
}

11
Models/ErrorViewModel.cs Normal file
View File

@ -0,0 +1,11 @@
namespace QRRapidoApp.Models
{
public class ErrorViewModel
{
public string? RequestId { get; set; }
public bool ShowRequestId => !string.IsNullOrEmpty(RequestId);
public string? ErrorCode { get; set; }
public string? ErrorMessage { get; set; }
}
}

34
Models/LogoReadability.cs Normal file
View File

@ -0,0 +1,34 @@
namespace QRRapidoApp.Models
{
public class LogoReadabilityInfo
{
public bool HasLogo { get; set; }
public int LogoSizePercent { get; set; }
public int ReadabilityScore { get; set; } // 0-100
public string DifficultyLevel { get; set; } = string.Empty; // VeryEasy, Easy, Medium, Hard, VeryHard
public string UserMessage { get; set; } = string.Empty;
public List<string> Tips { get; set; } = new();
public LogoComplexity LogoComplexity { get; set; } = new();
}
public class LogoComplexity
{
public int Width { get; set; }
public int Height { get; set; }
public bool IsSquare { get; set; }
public bool HasTransparency { get; set; }
public int DominantColorCount { get; set; }
public double ContrastRatio { get; set; }
public string FileFormat { get; set; } = string.Empty;
public int FileSizeBytes { get; set; }
}
public enum ReadabilityLevel
{
VeryEasy = 85, // 85-100
Easy = 70, // 70-84
Medium = 55, // 55-69
Hard = 40, // 40-54
VeryHard = 0 // 0-39
}
}

39
Models/Order.cs Normal file
View File

@ -0,0 +1,39 @@
using MongoDB.Bson;
using MongoDB.Bson.Serialization.Attributes;
namespace QRRapidoApp.Models
{
public class Order
{
[BsonId]
[BsonRepresentation(BsonType.ObjectId)]
public string Id { get; set; } = string.Empty;
[BsonElement("userId")]
public string UserId { get; set; } = string.Empty;
[BsonElement("userEmail")]
public string UserEmail { get; set; } = string.Empty;
[BsonElement("amount")]
public decimal Amount { get; set; } // Valor em R$ (ex: 10.00)
[BsonElement("creditsAmount")]
public int CreditsAmount { get; set; } // Quantidade de créditos comprados (ex: 10)
[BsonElement("pixCode")]
public string PixCode { get; set; } = string.Empty; // Código de identificação do PIX (ex: PED-12345)
[BsonElement("status")]
public string Status { get; set; } = "Pending"; // Pending, Paid, Cancelled
[BsonElement("createdAt")]
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
[BsonElement("paidAt")]
public DateTime? PaidAt { get; set; }
[BsonElement("approvedBy")]
public string? ApprovedBy { get; set; } // Email do admin que aprovou
}
}

View File

@ -6,18 +6,27 @@ namespace QRRapidoApp.Models
public class QRCodeHistory
{
[BsonId]
[BsonRepresentation(BsonType.ObjectId)]
[BsonRepresentation(BsonType.String)]
public string Id { get; set; } = string.Empty;
[BsonElement("userId")]
public string? UserId { get; set; } // null for anonymous users
[BsonElement("ipAddress")]
public string? IpAddress { get; set; }
[BsonElement("deviceId")]
public string? DeviceId { get; set; }
[BsonElement("type")]
public string Type { get; set; } = string.Empty; // URL, Text, WiFi, vCard, SMS, Email
[BsonElement("content")]
public string Content { get; set; } = string.Empty;
[BsonElement("contentHash")]
public string ContentHash { get; set; } = string.Empty; // SHA256 Hash for deduplication
[BsonElement("qrCodeBase64")]
public string QRCodeBase64 { get; set; } = string.Empty;
@ -33,9 +42,15 @@ namespace QRRapidoApp.Models
[BsonElement("scanCount")]
public int ScanCount { get; set; } = 0;
[BsonElement("costInCredits")]
public int CostInCredits { get; set; } = 0; // 0 = Free/Cache, 1 = Paid
[BsonElement("isDynamic")]
public bool IsDynamic { get; set; } = false;
[BsonElement("trackingId")]
public string? TrackingId { get; set; } // Public ID for tracking URL (premium feature)
[BsonElement("size")]
public int Size { get; set; } = 300;

42
Models/Rating.cs Normal file
View File

@ -0,0 +1,42 @@
using MongoDB.Bson;
using MongoDB.Bson.Serialization.Attributes;
namespace QRRapidoApp.Models
{
public class Rating
{
[BsonId]
[BsonRepresentation(BsonType.ObjectId)]
public string Id { get; set; } = string.Empty;
[BsonElement("rating")]
public int RatingValue { get; set; } // 1-5 stars
[BsonElement("name")]
public string? Name { get; set; }
[BsonElement("email")]
public string? Email { get; set; }
[BsonElement("comment")]
public string? Comment { get; set; }
[BsonElement("url")]
public string Url { get; set; } = string.Empty;
[BsonElement("userAgent")]
public string UserAgent { get; set; } = string.Empty;
[BsonElement("userId")]
public string? UserId { get; set; } // If authenticated
[BsonElement("createdAt")]
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
[BsonElement("ipAddress")]
public string? IpAddress { get; set; }
[BsonElement("culture")]
public string Culture { get; set; } = "pt-BR";
}
}

View File

@ -45,6 +45,9 @@ namespace QRRapidoApp.Models
[BsonElement("stripeSubscriptionId")]
public string? StripeSubscriptionId { get; set; }
[BsonElement("subscriptionStartedAt")]
public DateTime? SubscriptionStartedAt { get; set; } // Data de início da assinatura atual
[BsonElement("preferredLanguage")]
public string PreferredLanguage { get; set; } = "pt-BR";
@ -56,5 +59,49 @@ namespace QRRapidoApp.Models
[BsonElement("totalQRGenerated")]
public int TotalQRGenerated { get; set; } = 0;
// NEW: Credit System
[BsonElement("credits")]
public int Credits { get; set; } = 0;
[BsonElement("freeQRsUsed")]
public int FreeQRsUsed { get; set; } = 0; // Tracks usage of the 5 free QRs limit
[BsonElement("historyHashes")]
public List<string> HistoryHashes { get; set; } = new(); // Stores SHA256 hashes of generated content to prevent double charging
[BsonElement("apiKeys")]
public List<ApiKeyConfig> ApiKeys { get; set; } = new();
/// <summary>API subscription plan (separate from QR credits).</summary>
[BsonElement("apiSubscription")]
public ApiSubscription ApiSubscription { get; set; } = new();
}
public class ApiKeyConfig
{
[BsonElement("keyHash")]
public string KeyHash { get; set; } = string.Empty;
[BsonElement("prefix")]
public string Prefix { get; set; } = string.Empty; // Ex: qr_...
[BsonElement("name")]
public string Name { get; set; } = "Default";
[BsonElement("createdAt")]
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
[BsonElement("isActive")]
public bool IsActive { get; set; } = true;
[BsonElement("lastUsedAt")]
public DateTime? LastUsedAt { get; set; }
[BsonElement("planTier")]
public ApiPlanTier PlanTier { get; set; } = ApiPlanTier.Free;
[BsonElement("totalRequests")]
public long TotalRequests { get; set; } = 0;
}
}

View File

@ -0,0 +1,11 @@
namespace QRRapidoApp.Models.ViewModels
{
public class ArticleViewModel
{
public ArticleMetadata Metadata { get; set; } = new();
public string HtmlContent { get; set; } = string.Empty;
public string Slug { get; set; } = string.Empty;
public DateTime LastModified { get; set; }
public List<ArticleMetadata> RelatedArticles { get; set; } = new();
}
}

View File

@ -1,3 +1,5 @@
using QRRapidoApp.Models;
namespace QRRapidoApp.Models.ViewModels
{
public class QRGenerationRequest
@ -15,6 +17,29 @@ namespace QRRapidoApp.Models.ViewModels
public bool IsPremium { get; set; } = false;
public bool HasLogo { get; set; } = false;
public byte[]? Logo { get; set; }
// NOVAS PROPRIEDADES PARA LOGO APRIMORADO
/// <summary>
/// Tamanho do logo em porcentagem (10-25%). Padrão: 20%
/// </summary>
public int? LogoSizePercent { get; set; } = 20;
/// <summary>
/// Se deve aplicar a cor do QR code no logo (Premium feature)
/// </summary>
public bool ApplyLogoColorization { get; set; } = false;
/// <summary>
/// Se deve habilitar analytics de leitura (Premium feature)
/// Quando habilitado, o QR code usa URL de redirect para contabilizar leituras
/// </summary>
public bool EnableTracking { get; set; } = false;
/// <summary>
/// Output image format: "png" (default), "webp", "svg"
/// WebP is recommended for API consumers — smaller file size, same visual quality.
/// </summary>
public string OutputFormat { get; set; } = "png";
}
public class QRGenerationResult
@ -28,5 +53,9 @@ namespace QRRapidoApp.Models.ViewModels
public int? RemainingQRs { get; set; } // For free users
public bool Success { get; set; } = true;
public string? ErrorMessage { get; set; }
public string? Message { get; set; } // Feedback message (e.g. "Recovered from history")
public LogoReadabilityInfo? ReadabilityInfo { get; set; } // Nova propriedade para análise de legibilidade
public string? TrackingId { get; set; } // Tracking ID for analytics (Premium feature)
public string OutputFormat { get; set; } = "png"; // "png", "webp"
}
}

View File

@ -6,9 +6,13 @@ using Microsoft.AspNetCore.StaticFiles;
using Microsoft.Extensions.Caching.Distributed;
using Microsoft.Extensions.Localization;
using MongoDB.Driver;
using QRRapidoApp.Configuration;
using QRRapidoApp.Data;
using QRRapidoApp.Middleware;
using QRRapidoApp.Providers;
using QRRapidoApp.Services;
using QRRapidoApp.Models.Ads;
using QRRapidoApp.Services.Ads;
using QRRapidoApp.Services.Monitoring;
using QRRapidoApp.Services.HealthChecks;
using StackExchange.Redis;
@ -16,30 +20,91 @@ using Stripe;
using System.Globalization;
using Serilog;
using Serilog.Events;
using Serilog.Sinks.OpenSearch;
using Serilog.Sinks.SystemConsole.Themes;
using Microsoft.AspNetCore.Mvc.Razor;
using Microsoft.AspNetCore.HttpOverrides;
using Microsoft.AspNetCore.DataProtection;
using Microsoft.AspNetCore.RateLimiting;
using System.Threading.RateLimiting;
using Microsoft.AspNetCore.Server.Kestrel.Core;
using AspNetCore.DataProtection.MongoDb;
using Microsoft.OpenApi.Models;
var builder = WebApplication.CreateBuilder(args);
// Fix for WSL path issues - disable StaticWebAssets completely
var options = new WebApplicationOptions
{
Args = args,
ContentRootPath = Directory.GetCurrentDirectory(),
WebRootPath = Path.Combine(Directory.GetCurrentDirectory(), "wwwroot")
};
// Disable StaticWebAssets for WSL compatibility
Environment.SetEnvironmentVariable("ASPNETCORE_HOSTINGSTARTUP__STATICWEBASSETS__ENABLED", "false");
var builder = WebApplication.CreateBuilder(options);
// Add Docker Secrets as configuration source (for Swarm deployments)
// Secrets override values from appsettings.json
builder.Configuration.AddDockerSecrets();
// Configure Serilog
Log.Logger = new LoggerConfiguration()
var loggerConfig = new LoggerConfiguration()
.ReadFrom.Configuration(builder.Configuration)
.Enrich.FromLogContext()
.Enrich.WithEnvironmentName()
.Enrich.WithProcessId()
.Enrich.WithThreadId()
.Enrich.WithProperty("ApplicationName", builder.Configuration["ApplicationName"] ?? "QRRapido")
.Enrich.WithProperty("Environment", "Dev")
.Enrich.WithProperty("Environment", builder.Environment.EnvironmentName)
.WriteTo.Async(a => a.Console(
outputTemplate: "[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj} {Properties:j}{NewLine}{Exception}",
theme: AnsiConsoleTheme.Code))
.WriteTo.Async(a => a.Seq(builder.Configuration["Serilog:SeqUrl"],
apiKey: builder.Configuration["Serilog:ApiKey"]=="" ? null : builder.Configuration["Serilog:ApiKey"]))
.CreateLogger();
theme: AnsiConsoleTheme.Code));
var openSearchUrl = builder.Configuration["Serilog:OpenSearchUrl"];
if (!string.IsNullOrEmpty(openSearchUrl))
{
var environment = builder.Environment.EnvironmentName.ToLower();
var envMapping = environment switch
{
"Production" => "prod",
"Staging" => "staging",
"Development" => "dev",
_ => environment
};
var indexFormat = $"qrrapido-logs-{envMapping}-{{0:yyyy-MM-dd}}";
try
{
loggerConfig.WriteTo.Async(a => a.OpenSearch(new OpenSearchSinkOptions(new Uri(openSearchUrl))
{
IndexFormat = indexFormat,
AutoRegisterTemplate = true,
BufferBaseFilename = "./logs/buffer",
ModifyConnectionSettings = conn => conn
.RequestTimeout(TimeSpan.FromSeconds(30))
.PingTimeout(TimeSpan.FromSeconds(10)),
MinimumLogEventLevel = LogEventLevel.Information,
EmitEventFailure = EmitEventFailureHandling.WriteToSelfLog,
RegisterTemplateFailure = RegisterTemplateRecovery.IndexAnyway,
BatchPostingLimit = 50,
Period = TimeSpan.FromSeconds(5),
}), bufferSize: 10000, blockWhenFull: false);
}
catch (Exception ex)
{
// Fails silently, logs will continue on console.
loggerConfig.WriteTo.Console(outputTemplate: $"Error setting up OpenSearch sink: {ex.Message}");
}
}
Log.Logger = loggerConfig.CreateLogger();
builder.Host.UseSerilog();
// Add services to the container
builder.Services.AddControllersWithViews()
.AddViewLocalization(Microsoft.AspNetCore.Mvc.Razor.LanguageViewLocationExpanderFormat.Suffix)
.AddDataAnnotationsLocalization();
@ -79,31 +144,34 @@ else
builder.Services.AddScoped<MongoDbContext>();
}
// Cache Configuration - use Redis if available, otherwise memory cache
var redisConnectionString = builder.Configuration.GetConnectionString("Redis");
if (!string.IsNullOrEmpty(redisConnectionString))
builder.Services.AddMemoryCache();
builder.Services.AddSingleton<IDistributedCache, MemoryDistributedCacheWrapper>();
builder.Services.Configure<AdsConfigurationOptions>(builder.Configuration.GetSection("Ads"));
builder.Services.AddScoped<IAdSlotConfigurationProvider, ConfigurationAdSlotProvider>();
// ✅ DataProtection compartilhado via MongoDB (para múltiplas réplicas do Swarm)
if (!string.IsNullOrEmpty(mongoConnectionString))
{
try
{
builder.Services.AddStackExchangeRedisCache(options =>
{
options.Configuration = redisConnectionString;
});
}
catch
{
// Fallback to memory cache if Redis fails
builder.Services.AddMemoryCache();
builder.Services.AddSingleton<IDistributedCache, QRRapidoApp.Services.MemoryDistributedCacheWrapper>();
}
Log.Information("Configuring DataProtection to persist keys in MongoDB for Swarm compatibility");
builder.Services.AddDataProtection()
.SetApplicationName("QRRapido")
.PersistKeysToMongoDb(
() => new MongoClient(mongoConnectionString).GetDatabase("QRRapidoDB"),
"DataProtectionKeys");
}
else
{
// Use memory cache when Redis is not configured
builder.Services.AddMemoryCache();
builder.Services.AddSingleton<IDistributedCache, MemoryDistributedCacheWrapper>();
// Fallback para FileSystem em desenvolvimento
Log.Warning("MongoDB not available - using FileSystem for DataProtection (development only)");
var keysDirectory = Path.Combine(Directory.GetCurrentDirectory(), "keys");
Directory.CreateDirectory(keysDirectory);
builder.Services.AddDataProtection()
.PersistKeysToFileSystem(new DirectoryInfo(keysDirectory))
.SetApplicationName("QRRapido");
}
// Authentication Configuration
builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie(options =>
@ -121,7 +189,7 @@ builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationSc
// ADICIONE ESTAS LINHAS:
options.Events.OnRedirectToAuthorizationEndpoint = context =>
{
context.Response.Redirect(context.RedirectUri + "&prompt=select_account");
context.Response.Redirect(context.RedirectUri);
return Task.CompletedTask;
};
@ -134,7 +202,6 @@ builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationSc
options.ClientId = builder.Configuration["Authentication:Microsoft:ClientId"];
options.ClientSecret = builder.Configuration["Authentication:Microsoft:ClientSecret"];
// ADICIONE ESTAS LINHAS:
options.AuthorizationEndpoint = "https://login.microsoftonline.com/common/oauth2/v2.0/authorize";
options.TokenEndpoint = "https://login.microsoftonline.com/common/oauth2/v2.0/token";
@ -158,29 +225,33 @@ builder.Services.Configure<RequestLocalizationOptions>(options =>
{
new CultureInfo("pt-BR"),
new CultureInfo("es"),
new CultureInfo("en")
new CultureInfo("en"),
};
options.DefaultRequestCulture = new RequestCulture("pt-BR", "pt-BR");
options.SupportedCultures = supportedCultures;
options.SupportedUICultures = supportedCultures;
options.FallBackToParentCultures = true;
options.FallBackToParentUICultures = true;
options.FallBackToParentCultures = false;
options.FallBackToParentUICultures = false;
// Clear default providers and add custom ones in priority order
options.RequestCultureProviders.Clear();
options.RequestCultureProviders.Add(new RouteDataRequestCultureProvider());
options.RequestCultureProviders.Add(new CustomRouteDataRequestCultureProvider());
options.RequestCultureProviders.Add(new QueryStringRequestCultureProvider());
options.RequestCultureProviders.Add(new CookieRequestCultureProvider());
});
// Custom Services
builder.Services.AddScoped<IQRCodeService, QRRapidoService>();
builder.Services.AddScoped<IUserService, UserService>();
builder.Services.AddScoped<IPlanService, QRRapidoApp.Services.PlanService>();
builder.Services.AddScoped<IMarkdownService, MarkdownService>();
builder.Services.AddScoped<AdDisplayService>();
builder.Services.AddScoped<StripeService>();
builder.Services.AddScoped<LogoReadabilityAnalyzer>();
builder.Services.AddScoped<IQRBusinessManager, QRBusinessManager>();
builder.Services.AddScoped<IQuotaValidator, QuotaValidator>();
builder.Services.AddScoped<IApiRateLimitService, ApiRateLimitService>();
// Background Services
builder.Services.AddHostedService<HistoryCleanupService>();
@ -191,12 +262,14 @@ if (builder.Configuration.GetValue<bool>("ResourceMonitoring:Enabled", true))
builder.Services.AddHostedService<ResourceMonitoringService>();
}
if (builder.Configuration.GetValue<bool>("MongoDbMonitoring:Enabled", true))
{
builder.Services.AddHostedService<MongoDbMonitoringService>();
}
//if (builder.Configuration.GetValue<bool>("MongoDbMonitoring:Enabled", true))
//{
// builder.Services.AddHostedService<MongoDbMonitoringService>();
//}
// CORS for API endpoints
// CORS — two policies:
// AllowSpecificOrigins: for the site (MVC endpoints)
// ApiPolicy: for /api/v1/* (open to any origin, auth is via API Key header)
builder.Services.AddCors(options =>
{
options.AddPolicy("AllowSpecificOrigins", policy =>
@ -205,30 +278,154 @@ builder.Services.AddCors(options =>
.AllowAnyHeader()
.AllowAnyMethod();
});
});
options.AddPolicy("ApiPolicy", policy =>
{
policy.AllowAnyOrigin()
.AllowAnyMethod()
.AllowAnyHeader()
.WithExposedHeaders(
"X-RateLimit-Limit",
"X-RateLimit-Remaining",
"X-RateLimit-Reset",
"X-Quota-Limit",
"X-Quota-Remaining",
"Retry-After");
});
});
// Health checks with custom implementations
builder.Services.AddScoped<MongoDbHealthCheck>();
builder.Services.AddScoped<SeqHealthCheck>();
builder.Services.AddScoped<ResourceHealthCheck>();
builder.Services.AddScoped<ExternalServicesHealthCheck>();
builder.Services.AddHealthChecks()
.AddCheck<MongoDbHealthCheck>("mongodb")
.AddCheck<SeqHealthCheck>("seq")
.AddCheck<ResourceHealthCheck>("resources")
.AddCheck<ExternalServicesHealthCheck>("external_services");
builder.Services.Configure<ForwardedHeadersOptions>(options =>
{
options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto;
options.KnownProxies.Clear();
options.KnownNetworks.Clear();
});
builder.Services.AddRateLimiter(options =>
{
options.RejectionStatusCode = 429;
options.AddFixedWindowLimiter("api", options =>
{
options.PermitLimit = 600;
options.Window = TimeSpan.FromMinutes(1);
options.QueueProcessingOrder = QueueProcessingOrder.OldestFirst;
options.QueueLimit = 10;
});
});
builder.Services.Configure<KestrelServerOptions>(options =>
{
options.Limits.MaxConcurrentConnections = 2000;
options.Limits.RequestHeadersTimeout = TimeSpan.FromSeconds(30);
});
// Swagger / OpenAPI — exposed at /api/docs
builder.Services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new OpenApiInfo
{
Title = "QRRapido API",
Version = "v1",
Description =
"**QRRapido** — Ultra-fast QR Code generation API.\n\n" +
"Generate QR codes for URLs, Pix payments, Wi-Fi, vCards, WhatsApp, SMS and more.\n\n" +
"**PT-BR:** Gere QR codes para URLs, Pix, Wi-Fi, vCard, WhatsApp, SMS e muito mais.\n\n" +
"**Authentication:** All endpoints (except `/ping`) require an API key sent in the `X-API-Key` header.\n" +
"Get your key at [qrrapido.site/Developer](https://qrrapido.site/Developer).",
Contact = new OpenApiContact
{
Name = "QRRapido",
Url = new Uri("https://qrrapido.site"),
Email = "contato@qrrapido.site"
},
License = new OpenApiLicense
{
Name = "Commercial — see qrrapido.site/terms"
}
});
// API Key security scheme
c.AddSecurityDefinition("ApiKey", new OpenApiSecurityScheme
{
Type = SecuritySchemeType.ApiKey,
In = ParameterLocation.Header,
Name = "X-API-Key",
Description = "Your QRRapido API key. Obtain one at https://qrrapido.site/Developer"
});
c.AddSecurityRequirement(new OpenApiSecurityRequirement
{
{
new OpenApiSecurityScheme
{
Reference = new OpenApiReference
{
Type = ReferenceType.SecurityScheme,
Id = "ApiKey"
}
},
Array.Empty<string>()
}
});
// Only document routes under /api/v1/ — ignore the internal web API controllers
c.DocInclusionPredicate((_, apiDesc) =>
apiDesc.RelativePath?.StartsWith("api/v1/", StringComparison.OrdinalIgnoreCase) == true);
});
var app = builder.Build();
app.UseRateLimiter();
app.UseForwardedHeaders();
// Swagger UI — accessible at /api/docs (no auth required, it's documentation)
app.UseSwagger(options =>
{
options.RouteTemplate = "api/docs/{documentName}/openapi.json";
});
app.UseSwaggerUI(options =>
{
options.SwaggerEndpoint("/api/docs/v1/openapi.json", "QRRapido API v1");
options.RoutePrefix = "api/docs";
options.DocumentTitle = "QRRapido API Docs";
});
// Configure the HTTP request pipeline
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Home/Error");
// For API routes (/api/v1/*): return JSON errors — no stack traces
app.UseExceptionHandler(errorApp =>
{
errorApp.Run(async context =>
{
if (!context.Response.HasStarted)
{
context.Response.ContentType = "application/json";
context.Response.StatusCode = 500;
await context.Response.WriteAsJsonAsync(new
{
error = "An internal error occurred. Please try again later.",
requestId = context.TraceIdentifier
});
}
});
});
app.UseHsts();
app.UseHttpsRedirection();
}
app.UseHttpsRedirection();
// Security headers + JSON exception handler for /api/v1/* routes
app.UseMiddleware<QRRapidoApp.Middleware.ApiSecurityHeadersMiddleware>();
app.UseStaticFiles();
@ -237,11 +434,11 @@ app.UseMiddleware<LanguageRedirectionMiddleware>();
app.UseRouting();
app.UseCors("AllowSpecificOrigins");
// Localization middleware
// Localization middleware (after routing so route data is available)
app.UseRequestLocalization();
app.UseCors("AllowSpecificOrigins");
app.UseAuthentication();
app.UseAuthorization();
@ -251,7 +448,7 @@ app.UseSession();
app.UseMiddleware<LastLoginUpdateMiddleware>();
// Health check endpoint
app.MapHealthChecks("/health");
app.MapHealthChecks("/healthcheck");
//app.MapControllerRoute(
// name: "auth",
@ -263,15 +460,14 @@ app.MapHealthChecks("/health");
// pattern: "Account/{action}",
// defaults: new { controller = "Account" });
// Language routes (must be first)
// Language routes (must be first for WEB)
app.MapControllerRoute(
name: "localized",
pattern: "{culture:regex(^(pt-BR|es|en)$)}/{controller=Home}/{action=Index}/{id?}");
// API routes
app.MapControllerRoute(
name: "api",
pattern: "api/{controller}/{action=Index}/{id?}");
name: "api_v1",
pattern: "api/v1/{controller}/{action}/{id?}");
// Default fallback route (for development/testing without culture)
app.MapControllerRoute(
@ -290,4 +486,4 @@ catch (Exception ex)
finally
{
Log.CloseAndFlush();
}
}

View File

@ -1,12 +1,12 @@
{
"profiles": {
"QRRapidoApp": {
"commandName": "Project",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"applicationUrl": "https://localhost:52428;https://192.168.0.85:52428;http://localhost:52429"
}
}
{
"profiles": {
"QRRapidoApp": {
"commandName": "Project",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"applicationUrl": "https://localhost:52428;http://localhost:52429"
}
}
}

View File

@ -0,0 +1,38 @@
using Microsoft.AspNetCore.Localization;
namespace QRRapidoApp.Providers
{
public class CustomRouteDataRequestCultureProvider : IRequestCultureProvider
{
public Task<ProviderCultureResult?> DetermineProviderCultureResult(HttpContext httpContext)
{
// First check if the middleware has already determined the culture (e.g. for static routes like /es/pix)
if (httpContext.Items.TryGetValue("Culture", out var cultureItem) && cultureItem is string cultureFromMiddleware)
{
var supportedCultures = new[] { "pt-BR", "es", "en" };
if (supportedCultures.Contains(cultureFromMiddleware))
{
return Task.FromResult<ProviderCultureResult?>(new ProviderCultureResult(cultureFromMiddleware, cultureFromMiddleware));
}
}
// Fallback to route data (standard routing)
var routeValues = httpContext.GetRouteData()?.Values;
if (routeValues != null && routeValues.TryGetValue("culture", out var cultureValue))
{
var culture = cultureValue?.ToString();
if (!string.IsNullOrEmpty(culture))
{
// Map the supported cultures
var supportedCultures = new[] { "pt-BR", "es", "en" };
if (supportedCultures.Contains(culture))
{
return Task.FromResult<ProviderCultureResult?>(new ProviderCultureResult(culture, culture));
}
}
}
return Task.FromResult<ProviderCultureResult?>(null);
}
}
}

View File

@ -9,6 +9,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="AspNetCore.DataProtection.MongoDb" Version="2.1.0" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.Google" Version="8.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.MicrosoftAccount" Version="8.0.0" />
<PackageReference Include="MongoDB.Driver" Version="2.22.0" />
@ -20,28 +21,48 @@
<PackageReference Include="Serilog.Enrichers.Thread" Version="4.0.0" />
<PackageReference Include="Serilog.Sinks.Async" Version="2.1.0" />
<PackageReference Include="Serilog.Sinks.Console" Version="6.0.1-dev-00953" />
<PackageReference Include="Serilog.Sinks.Seq" Version="9.0.0" />
<PackageReference Include="Stripe.net" Version="43.15.0" />
<PackageReference Include="Serilog.Sinks.OpenSearch" Version="1.3.0" />
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.7" />
<PackageReference Include="SixLabors.ImageSharp.Drawing" Version="2.1.4" />
<PackageReference Include="Stripe.net" Version="48.4.0" />
<PackageReference Include="StackExchange.Redis" Version="2.7.4" />
<PackageReference Include="Microsoft.Extensions.Caching.StackExchangeRedis" Version="8.0.0" />
<PackageReference Include="System.Drawing.Common" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Localization" Version="8.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Localization" Version="2.2.0" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.9.0" />
<PackageReference Include="xunit.assert" Version="2.9.3" />
<PackageReference Include="xunit.extensibility.core" Version="2.9.3" />
<PackageReference Include="Markdig" Version="0.37.0" />
<PackageReference Include="YamlDotNet" Version="16.2.0" />
</ItemGroup>
<ItemGroup>
<Folder Include="wwwroot\images\" />
<Folder Include="wwwroot\css\" />
<Folder Include="wwwroot\js\" />
<Folder Include="Data\" />
<Folder Include="Services\" />
<Folder Include="Models\" />
<Folder Include="Middleware\" />
<Folder Include="Tests\" />
</ItemGroup>
<ItemGroup>
<!-- Exclude E2E test project files from main project compilation -->
<Compile Remove="Tests.E2E/**/*.cs" />
<Content Remove="Tests.E2E/**" />
<None Remove="Tests.E2E/**" />
</ItemGroup>
<ItemGroup>
<Content Include="Content\Tutoriais\*.md">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
</Content>
<Content Include="Content\DevTutoriais\*.md">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
</Content>
</ItemGroup>
<ItemGroup>
<Compile Update="Resources\SharedResource.Designer.cs">
<DesignTime>True</DesignTime>
@ -66,4 +87,10 @@
</EmbeddedResource>
</ItemGroup>
</Project>
<Target Name="BuildFrontend" BeforeTargets="Build" Condition="'$(Configuration)'=='Release'">
<Message Importance="High" Text="Executando build do frontend com Vite..." />
<Exec Command="npm install" WorkingDirectory="$(MSBuildProjectDirectory)" Condition="!Exists('node_modules')" />
<Exec Command="npm run build" WorkingDirectory="$(MSBuildProjectDirectory)" />
</Target>
</Project>

View File

@ -1,25 +1,36 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.13.35818.85 d17.13
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "QRRapidoApp", "QRRapidoApp.csproj", "{8AF92774-40E8-830E-08B3-67F0A0B91DDE}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{8AF92774-40E8-830E-08B3-67F0A0B91DDE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{8AF92774-40E8-830E-08B3-67F0A0B91DDE}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8AF92774-40E8-830E-08B3-67F0A0B91DDE}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8AF92774-40E8-830E-08B3-67F0A0B91DDE}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {9E53D8E2-0957-4925-B347-404E3B14587B}
EndGlobalSection
EndGlobal

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.13.35818.85
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "QRRapidoApp", "QRRapidoApp.csproj", "{8AF92774-40E8-830E-08B3-67F0A0B91DDE}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tests.E2E", "Tests.E2E\Tests.E2E.csproj", "{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "RootFolder", "RootFolder", "{02EA681E-C7D8-13C7-8484-4AC65E1B71E8}"
ProjectSection(SolutionItems) = preProject
.github\workflows\deploy.yml = .github\workflows\deploy.yml
EndProjectSection
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{8AF92774-40E8-830E-08B3-67F0A0B91DDE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{8AF92774-40E8-830E-08B3-67F0A0B91DDE}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8AF92774-40E8-830E-08B3-67F0A0B91DDE}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8AF92774-40E8-830E-08B3-67F0A0B91DDE}.Release|Any CPU.Build.0 = Release|Any CPU
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {9E53D8E2-0957-4925-B347-404E3B14587B}
EndGlobalSection
EndGlobal

File diff suppressed because it is too large Load Diff

Binary file not shown.

View File

@ -14,7 +14,6 @@
- 👑 **Sistema Premium**: Stripe integrado com recursos avançados
- 📱 **PWA Ready**: Funciona como aplicativo móvel
- 🎨 **UI Moderna**: Bootstrap 5 com design focado em velocidade
- 📊 **Analytics**: Google Analytics 4 integrado
- 🏗️ **Escalável**: MongoDB + Redis para alta performance
## 🎯 Funcionalidades por Usuário
@ -36,7 +35,6 @@
- ✅ **Sem anúncios permanentemente**
- ✅ Geração prioritária (0.4s)
- ✅ QR codes dinâmicos (editáveis)
- ✅ Analytics avançados
- ✅ Suporte prioritário
- ✅ API access
@ -342,4 +340,8 @@ Este projeto está licenciado sob a licença MIT - veja o arquivo [LICENSE](LICE
**QR Rapido** - Velocidade extrema meets experiência excepcional 🚀
*Desenvolvido com ❤️ para a comunidade de desenvolvedores*
*Desenvolvido com ❤️ para a comunidade de desenvolvedores*
No WSL - Para rodar o webhook do stripe local.
```bash
stripe listen --forward-to https://localhost:52428/pagamento/stripewebhook --skip-verify

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,271 +1,486 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="Tagline" xml:space="preserve">
<value>Generate QR codes in seconds!</value>
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="Tagline" xml:space="preserve">
<value>Generate QR codes in seconds!</value>
</data>
<data name="GenerateQR" xml:space="preserve">
<value>Generate QR Code</value>
</data>
<data name="QRType" xml:space="preserve">
<value>QR Code Type</value>
</data>
<data name="Content" xml:space="preserve">
<value>Content</value>
</data>
<data name="URLType" xml:space="preserve">
<value>URL/Link</value>
</data>
<data name="TextType" xml:space="preserve">
<value>Plain Text</value>
</data>
<data name="WiFiType" xml:space="preserve">
<value>WiFi</value>
</data>
<data name="VCardType" xml:space="preserve">
<value>Business Card</value>
</data>
<data name="SMSType" xml:space="preserve">
<value>SMS</value>
</data>
<data name="EmailType" xml:space="preserve">
<value>Email</value>
</data>
<data name="DynamicType" xml:space="preserve">
<value>Dynamic QR (Premium)</value>
</data>
<data name="QuickStyle" xml:space="preserve">
<value>Quick Style</value>
</data>
<data name="ClassicStyle" xml:space="preserve">
<value>Classic</value>
</data>
<data name="ModernStyle" xml:space="preserve">
<value>Modern</value>
</data>
<data name="ColorfulStyle" xml:space="preserve">
<value>Colorful</value>
</data>
<data name="ContentPlaceholder" xml:space="preserve">
<value>Enter your QR code content here...</value>
</data>
<data name="AdvancedCustomization" xml:space="preserve">
<value>Advanced Customization</value>
</data>
<data name="PrimaryColor" xml:space="preserve">
<value>Primary Color</value>
</data>
<data name="BackgroundColor" xml:space="preserve">
<value>Background Color</value>
</data>
<data name="Size" xml:space="preserve">
<value>Size</value>
</data>
<data name="Margin" xml:space="preserve">
<value>Margin</value>
</data>
<data name="Logo" xml:space="preserve">
<value>Logo/Icon</value>
</data>
<data name="CornerStyle" xml:space="preserve">
<value>Corner Style</value>
</data>
<data name="GenerateRapidly" xml:space="preserve">
<value>Generate QR Code Rapidly</value>
</data>
<data name="Preview" xml:space="preserve">
<value>Preview</value>
</data>
<data name="PreviewPlaceholder" xml:space="preserve">
<value>Your QR code will appear here in seconds</value>
</data>
<data name="UltraFastGeneration" xml:space="preserve">
<value>Ultra-fast generation guaranteed</value>
</data>
<data name="DownloadPNG" xml:space="preserve">
<value>Download PNG</value>
</data>
<data name="DownloadSVG" xml:space="preserve">
<value>Download SVG (Vector)</value>
</data>
<data name="DownloadPDF" xml:space="preserve">
<value>Download PDF</value>
</data>
<data name="SaveToHistory" xml:space="preserve">
<value>Save to History</value>
</data>
<data name="LoginToSave" xml:space="preserve">
<value>Login to save to history</value>
</data>
<data name="PremiumTitle" xml:space="preserve">
<value>QR Rapido Premium</value>
</data>
<data name="SpeedTipsTitle" xml:space="preserve">
<value>Tips for Faster QR</value>
</data>
<data name="SpeedTip1" xml:space="preserve">
<value>Short URLs generate faster</value>
</data>
<data name="SpeedTip2" xml:space="preserve">
<value>Less text = higher speed</value>
</data>
<data name="SpeedTip3" xml:space="preserve">
<value>Solid colors optimize the process</value>
</data>
<data name="SpeedTip4" xml:space="preserve">
<value>Smaller sizes speed up downloads</value>
</data>
<data name="Login" xml:space="preserve">
<value>Login</value>
</data>
<data name="LoginWith" xml:space="preserve">
<value>Login with</value>
</data>
<data name="Google" xml:space="preserve">
<value>Google</value>
</data>
<data name="Microsoft" xml:space="preserve">
<value>Microsoft</value>
</data>
<data name="AdFreeOffer" xml:space="preserve">
<value>Unlock all premium features!</value>
</data>
<data name="SpecialOffer" xml:space="preserve">
<value>Premium Offer!</value>
</data>
<data name="LoginBenefits" xml:space="preserve">
<value>Remove ads, access advanced customization and much more for only $12.90/month or $129.00/year. Login and subscribe now!</value>
</data>
<data name="Privacy" xml:space="preserve">
<value>Privacy Policy</value>
</data>
<data name="BackToGenerator" xml:space="preserve">
<value>Back to generator</value>
</data>
<data name="GeneratedIn" xml:space="preserve">
<value>Generated in</value>
</data>
<data name="Seconds" xml:space="preserve">
<value>s</value>
</data>
<data name="UltraFast" xml:space="preserve">
<value>Ultra fast generation!</value>
</data>
<data name="Fast" xml:space="preserve">
<value>Fast generation!</value>
</data>
<data name="Normal" xml:space="preserve">
<value>Normal generation</value>
</data>
<data name="Error" xml:space="preserve">
<value>Generation error. Try again.</value>
</data>
<data name="Success" xml:space="preserve">
<value>QR Code saved to history!</value>
</data>
<data name="CreateQRCodeQuickly" xml:space="preserve">
<value>Create QR Code Quickly</value>
</data>
<data name="PremiumUserActive" xml:space="preserve">
<value>Premium User Active</value>
</data>
<data name="NoAdsHistoryUnlimitedQR" xml:space="preserve">
<value>No Ads • History • Unlimited QR</value>
</data>
<data name="UnlimitedToday" xml:space="preserve">
<value>Unlimited today</value>
</data>
<data name="QRCodesRemaining" xml:space="preserve">
<value>QR codes remaining</value>
</data>
<data name="QRCodeType" xml:space="preserve">
<value>QR Code Type</value>
</data>
<data name="SelectType" xml:space="preserve">
<value>Select type</value>
</data>
<data name="URLLink" xml:space="preserve">
<value>URL/Link</value>
</data>
<data name="SimpleText" xml:space="preserve">
<value>Simple Text</value>
</data>
<data name="VCard" xml:space="preserve">
<value>Business Card</value>
</data>
<data name="DynamicQRPremium" xml:space="preserve">
<value>Dynamic QR (Premium)</value>
</data>
<data name="EnterQRCodeContent" xml:space="preserve">
<value>Enter your QR code content here...</value>
</data>
<data name="ContentHints" xml:space="preserve">
<value>Content hints</value>
</data>
<data name="Classic" xml:space="preserve">
<value>Classic</value>
</data>
<data name="Modern" xml:space="preserve">
<value>Modern</value>
</data>
<data name="Colorful" xml:space="preserve">
<value>Colorful</value>
</data>
<data name="WPARecommended" xml:space="preserve">
<value>Rede WPA (a mais comum)</value>
</data>
<data name="WEPLegacy" xml:space="preserve">
<value>WEP (muito antigo)</value>
</data>
<data name="OpenNetwork" xml:space="preserve">
<value>Sem senha </value>
</data>
<data name="RequiredContent" xml:space="preserve">
<value>Content is required</value>
</data>
<data name="ContentTooLong" xml:space="preserve">
<value>Content too long. Maximum 4000 characters.</value>
</data>
<data name="PremiumCornerStyleRequired" xml:space="preserve">
<value>Custom corner styles are exclusive to Premium plan. Upgrade to use this functionality.</value>
</data>
<data name="RateLimitReached" xml:space="preserve">
<value>QR codes limit reached</value>
</data>
<data name="PremiumLogoRequired" xml:space="preserve">
<value>Custom logo is exclusive to Premium plan. Upgrade to use this functionality.</value>
</data>
<data name="LogoTooLarge" xml:space="preserve">
<value>Logo too large. Maximum 2MB.</value>
</data>
<data name="InvalidLogoFormat" xml:space="preserve">
<value>Invalid format. Use PNG or JPG.</value>
</data>
<data name="UserProfileTitle" xml:space="preserve">
<value>User Profile</value>
</data>
<data name="HistoryTitle" xml:space="preserve">
<value>QR Codes History</value>
</data>
<data name="ErrorSavingHistory" xml:space="preserve">
<value>Error saving to history.</value>
</data>
<data name="FeatureNotAvailable" xml:space="preserve">
<value>Feature not available</value>
</data>
<data name="ShareError" xml:space="preserve">
<value>Error sharing. Try another method.</value>
</data>
<data name="LinkCopied" xml:space="preserve">
<value>Link copied to clipboard!</value>
</data>
<data name="EnterQRContent" xml:space="preserve">
<value>Enter QR code content</value>
</data>
<data name="ValidationContentMinLength" xml:space="preserve">
<value>Content must have at least 3 characters</value>
</data>
<data name="VCardValidationError" xml:space="preserve">
<value>VCard validation error: </value>
</data>
<data name="FastestGeneratorBrazil" xml:space="preserve">
<value>QR Code generated with QR Rapido - the fastest generator in Brazil!</value>
</data>
<data name="QRGenerateDescription" xml:space="preserve">
<value>QR Rapido: Generate QR codes in seconds! Ultra-fast generator. Free, no registration required. 30 days ad-free after login.</value>
</data>
<data name="LogoNotProvided" xml:space="preserve">
<value>Logo not provided</value>
</data>
<data name="LogoTooSmall" xml:space="preserve">
<value>Logo too small. Minimum 32x32 pixels.</value>
</data>
<data name="InvalidImageFormat" xml:space="preserve">
<value>Invalid image format</value>
</data>
<data name="ErrorProcessingLogo" xml:space="preserve">
<value>Error processing logo.</value>
</data>
<data name="DeleteQRCode" xml:space="preserve">
<value>Delete QR Code</value>
</data>
<data name="ConfirmDeleteTitle" xml:space="preserve">
<value>Confirm Deletion</value>
</data>
<data name="ConfirmDeleteMessage" xml:space="preserve">
<value>Are you sure you want to delete this QR code from your history?</value>
</data>
<data name="Yes" xml:space="preserve">
<value>Yes</value>
</data>
<data name="No" xml:space="preserve">
<value>No</value>
</data>
<data name="QRCodeDeleted" xml:space="preserve">
<value>QR Code deleted successfully!</value>
</data>
<data name="ErrorDeletingQR" xml:space="preserve">
<value>Error deleting QR code. Please try again.</value>
</data>
<data name="GenerateQR" xml:space="preserve">
<value>Generate QR Code</value>
<data name="Deleting" xml:space="preserve">
<value>Deleting</value>
</data>
<data name="QRType" xml:space="preserve">
<value>QR Code Type</value>
<data name="PremiumSupportFabButton" xml:space="preserve">
<value>Talk with our team</value>
</data>
<data name="Content" xml:space="preserve">
<value>Content</value>
<data name="PremiumSupportMenuIntro" xml:space="preserve">
<value>Choose how you&apos;d like to reach us:</value>
</data>
<data name="URLType" xml:space="preserve">
<value>URL/Link</value>
<data name="PremiumSupportOptionTelegram" xml:space="preserve">
<value>Chat on Telegram</value>
</data>
<data name="TextType" xml:space="preserve">
<value>Plain Text</value>
<data name="PremiumSupportOptionForm" xml:space="preserve">
<value>Send contact form</value>
</data>
<data name="WiFiType" xml:space="preserve">
<value>WiFi</value>
<data name="PremiumSupportFormPageTitle" xml:space="preserve">
<value>Premium Support Contact</value>
</data>
<data name="VCardType" xml:space="preserve">
<value>Business Card</value>
<data name="PremiumSupportFormHeading" xml:space="preserve">
<value>Talk to our premium support team</value>
</data>
<data name="SMSType" xml:space="preserve">
<value>SMS</value>
<data name="PremiumSupportFormDescription" xml:space="preserve">
<value>Fill out the form below and we will reach you back as soon as possible.</value>
</data>
<data name="EmailType" xml:space="preserve">
<data name="PremiumSupportFormNameLabel" xml:space="preserve">
<value>Your name</value>
</data>
<data name="PremiumSupportFormNamePlaceholder" xml:space="preserve">
<value>How should we call you?</value>
</data>
<data name="PremiumSupportFormEmailLabel" xml:space="preserve">
<value>Your email</value>
</data>
<data name="PremiumSupportFormEmailPlaceholder" xml:space="preserve">
<value>you@example.com</value>
</data>
<data name="PremiumSupportFormMessageLabel" xml:space="preserve">
<value>Your message</value>
</data>
<data name="PremiumSupportFormMessagePlaceholder" xml:space="preserve">
<value>Tell us how we can help…</value>
</data>
<data name="PremiumSupportFormPreferredChannelLabel" xml:space="preserve">
<value>Preferred contact channel</value>
</data>
<data name="PremiumSupportFormChannelTelegram" xml:space="preserve">
<value>Telegram</value>
</data>
<data name="PremiumSupportFormChannelEmail" xml:space="preserve">
<value>Email</value>
</data>
<data name="DynamicType" xml:space="preserve">
<value>Dynamic QR (Premium)</value>
<data name="PremiumSupportFormChannelPhone" xml:space="preserve">
<value>Phone / WhatsApp</value>
</data>
<data name="QuickStyle" xml:space="preserve">
<value>Quick Style</value>
<data name="PremiumSupportFormSubmitLabel" xml:space="preserve">
<value>Send message</value>
</data>
<data name="ClassicStyle" xml:space="preserve">
<value>Classic</value>
<data name="PremiumSupportFormPrivacyDisclaimer" xml:space="preserve">
<value>We use this information only to respond to your request. Messages are processed via Formspree.</value>
</data>
<data name="ModernStyle" xml:space="preserve">
<value>Modern</value>
</data>
<data name="ColorfulStyle" xml:space="preserve">
<value>Colorful</value>
</data>
<data name="ContentPlaceholder" xml:space="preserve">
<value>Enter your QR code content here...</value>
</data>
<data name="AdvancedCustomization" xml:space="preserve">
<value>Advanced Customization</value>
</data>
<data name="PrimaryColor" xml:space="preserve">
<value>Primary Color</value>
</data>
<data name="BackgroundColor" xml:space="preserve">
<value>Background Color</value>
</data>
<data name="Size" xml:space="preserve">
<value>Size</value>
</data>
<data name="Margin" xml:space="preserve">
<value>Margin</value>
</data>
<data name="Logo" xml:space="preserve">
<value>Logo/Icon</value>
</data>
<data name="CornerStyle" xml:space="preserve">
<value>Corner Style</value>
</data>
<data name="GenerateRapidly" xml:space="preserve">
<value>Generate QR Code Rapidly</value>
</data>
<data name="Preview" xml:space="preserve">
<value>Preview</value>
</data>
<data name="PreviewPlaceholder" xml:space="preserve">
<value>Your QR code will appear here in seconds</value>
</data>
<data name="UltraFastGeneration" xml:space="preserve">
<value>Ultra-fast generation guaranteed</value>
</data>
<data name="DownloadPNG" xml:space="preserve">
<value>Download PNG</value>
</data>
<data name="DownloadSVG" xml:space="preserve">
<value>Download SVG (Vector)</value>
</data>
<data name="DownloadPDF" xml:space="preserve">
<value>Download PDF</value>
</data>
<data name="SaveToHistory" xml:space="preserve">
<value>Save to History</value>
</data>
<data name="LoginToSave" xml:space="preserve">
<value>Login to save to history</value>
</data>
<data name="PremiumTitle" xml:space="preserve">
<value>QR Rapido Premium</value>
</data>
<data name="SpeedTipsTitle" xml:space="preserve">
<value>Tips for Faster QR</value>
</data>
<data name="SpeedTip1" xml:space="preserve">
<value>Short URLs generate faster</value>
</data>
<data name="SpeedTip2" xml:space="preserve">
<value>Less text = higher speed</value>
</data>
<data name="SpeedTip3" xml:space="preserve">
<value>Solid colors optimize the process</value>
</data>
<data name="SpeedTip4" xml:space="preserve">
<value>Smaller sizes speed up downloads</value>
</data>
<data name="Login" xml:space="preserve">
<value>Login</value>
</data>
<data name="LoginWith" xml:space="preserve">
<value>Login with</value>
</data>
<data name="Google" xml:space="preserve">
<value>Google</value>
</data>
<data name="Microsoft" xml:space="preserve">
<value>Microsoft</value>
</data>
<data name="AdFreeOffer" xml:space="preserve">
<value>Unlock all premium features!</value>
</data>
<data name="SpecialOffer" xml:space="preserve">
<value>Premium Offer!</value>
</data>
<data name="LoginBenefits" xml:space="preserve">
<value>Remove ads, access detailed analytics and much more for only $12.90/month or $129.00/year. Login and subscribe now!</value>
</data>
<data name="Privacy" xml:space="preserve">
<value>Privacy Policy</value>
</data>
<data name="BackToGenerator" xml:space="preserve">
<value>Back to generator</value>
</data>
<data name="GeneratedIn" xml:space="preserve">
<value>Generated in</value>
</data>
<data name="Seconds" xml:space="preserve">
<value>s</value>
</data>
<data name="UltraFast" xml:space="preserve">
<value>Ultra fast generation!</value>
</data>
<data name="Fast" xml:space="preserve">
<value>Fast generation!</value>
</data>
<data name="Normal" xml:space="preserve">
<value>Normal generation</value>
</data>
<data name="Error" xml:space="preserve">
<value>Generation error. Try again.</value>
</data>
<data name="Success" xml:space="preserve">
<value>QR Code saved to history!</value>
</data>
<data name="CreateQRCodeQuickly" xml:space="preserve">
<value>Create QR Code Quickly</value>
</data>
<data name="PremiumUserActive" xml:space="preserve">
<value>Premium User Active</value>
</data>
<data name="NoAdsHistoryUnlimitedQR" xml:space="preserve">
<value>No Ads • History • Unlimited QR</value>
</data>
<data name="UnlimitedToday" xml:space="preserve">
<value>Unlimited today</value>
</data>
<data name="QRCodesRemaining" xml:space="preserve">
<value>QR codes remaining</value>
</data>
<data name="QRCodeType" xml:space="preserve">
<value>QR Code Type</value>
</data>
<data name="SelectType" xml:space="preserve">
<value>Select type</value>
</data>
<data name="URLLink" xml:space="preserve">
<value>URL/Link</value>
</data>
<data name="SimpleText" xml:space="preserve">
<value>Simple Text</value>
</data>
<data name="VCard" xml:space="preserve">
<value>Business Card</value>
</data>
<data name="DynamicQRPremium" xml:space="preserve">
<value>Dynamic QR (Premium)</value>
</data>
<data name="EnterQRCodeContent" xml:space="preserve">
<value>Enter your QR code content here...</value>
</data>
<data name="ContentHints" xml:space="preserve">
<value>Content hints</value>
</data>
<data name="Classic" xml:space="preserve">
<value>Classic</value>
</data>
<data name="Modern" xml:space="preserve">
<value>Modern</value>
</data>
<data name="Colorful" xml:space="preserve">
<value>Colorful</value>
</data>
</root>
</root>

272
Scripts/README-ADMIN-API.md Normal file
View File

@ -0,0 +1,272 @@
# 🔧 Admin API - Gerenciamento de Planos
Este diretório contém ferramentas para gerenciar os planos do Stripe no MongoDB via API local.
## 🔒 Segurança
**IMPORTANTE**: O endpoint `/api/Admin/*` **só funciona quando acessado de localhost** (127.0.0.1). Qualquer acesso externo retornará `403 Forbidden`.
---
## 📋 Arquivos
- **`plans.json`** - Configuração dos planos com Price IDs do Stripe
- **`update-plans.sh`** - Script bash para atualizar planos via curl
- **`seed-mongodb-plans.js`** - Script MongoDB direto (método alternativo)
---
## 🚀 Método 1: API Endpoint (Recomendado)
### Vantagens
- ✅ Não precisa de credenciais do MongoDB
- ✅ Validação automática
- ✅ Logs no sistema
- ✅ Funciona com app rodando
### Como Usar
#### 1⃣ Edite o `plans.json` com seus Price IDs
```json
{
"pricesByCountry": {
"BR": {
"stripePriceId": "price_SEU_ID_AQUI"
}
}
}
```
#### 2⃣ Inicie a aplicação
```bash
dotnet run
# ou
dotnet watch run
```
#### 3⃣ Execute o script de atualização
```bash
# Linux/Mac/WSL
cd Scripts
./update-plans.sh
# Especificar porta diferente
./update-plans.sh 5000
```
#### 4⃣ Ou use curl diretamente
```bash
# Atualizar planos
curl -k -X POST \
-H "Content-Type: application/json" \
-d @Scripts/plans.json \
https://localhost:52428/api/Admin/SeedPlans
# Listar planos
curl -k https://localhost:52428/api/Admin/Plans
# Deletar todos os planos
curl -k -X DELETE https://localhost:52428/api/Admin/Plans
```
---
## 🗄️ Método 2: Script MongoDB Direto
### Vantagens
- ✅ Funciona sem a app rodando
- ✅ Acesso direto ao MongoDB
### Desvantagens
- ❌ Precisa de credenciais do MongoDB
- ❌ Sem validação automática
### Como Usar
```bash
# Localhost
mongosh "mongodb://localhost:27017/QrRapido" < Scripts/seed-mongodb-plans.js
# Produção
mongosh "mongodb://user:pass@host:27017/QrRapido?replicaSet=rs0&authSource=admin" < Scripts/seed-mongodb-plans.js
```
---
## 📝 Endpoints Disponíveis
### POST `/api/Admin/SeedPlans`
Cria ou atualiza planos no MongoDB
**Request Body:**
```json
[
{
"interval": "month",
"stripePriceId": "price_...",
"pricesByCountry": { ... }
}
]
```
**Response:**
```json
{
"success": true,
"message": "2 plans seeded successfully",
"plans": [ ... ]
}
```
---
### GET `/api/Admin/Plans`
Lista todos os planos do MongoDB
**Response:**
```json
{
"success": true,
"count": 2,
"plans": [ ... ]
}
```
---
### DELETE `/api/Admin/Plans`
Remove todos os planos do MongoDB
**Response:**
```json
{
"success": true,
"deletedCount": 2
}
```
---
## 🛡️ Proteção de Segurança
O código verifica o IP de origem:
```csharp
var remoteIp = HttpContext.Connection.RemoteIpAddress;
var isLocalhost = remoteIp != null &&
(remoteIp.ToString() == "127.0.0.1" ||
remoteIp.ToString() == "::1" ||
remoteIp.ToString() == "localhost");
if (!isLocalhost) {
return Forbid("This endpoint is only accessible from localhost");
}
```
**Tentativas de acesso externo são logadas:**
```
[Warning] Unauthorized admin access attempt from 192.168.1.100
```
---
## 🧪 Testando
```bash
# 1. Listar planos atuais
curl -k https://localhost:52428/api/Admin/Plans | jq '.'
# 2. Deletar planos
curl -k -X DELETE https://localhost:52428/api/Admin/Plans
# 3. Criar novos planos
curl -k -X POST \
-H "Content-Type: application/json" \
-d @Scripts/plans.json \
https://localhost:52428/api/Admin/SeedPlans | jq '.'
# 4. Verificar se foram criados
curl -k https://localhost:52428/api/Admin/Plans | jq '.plans[] | {interval, priceIds: .pricesByCountry}'
```
---
## ⚠️ Troubleshooting
### Erro: "Connection refused"
- ✅ Certifique-se que a app está rodando: `dotnet run`
### Erro: "Forbidden" mesmo em localhost
- ✅ Verifique se está usando `https://localhost` (não `http://`)
- ✅ Verifique se está usando `localhost` ou `127.0.0.1` (não o IP da máquina)
### Erro: "SSL certificate problem"
- ✅ Use o flag `-k` no curl para aceitar certificados auto-assinados
---
## 📊 Estrutura do plans.json
```json
[
{
"name": {
"pt-BR": "Nome em português",
"es-PY": "Nombre en español",
"en": "Name in english"
},
"description": { ... },
"features": { ... },
"interval": "month" ou "year",
"stripePriceId": "price_default",
"pricesByCountry": {
"BR": {
"amount": 9.90,
"currency": "BRL",
"stripePriceId": "price_BR_ID"
},
"PY": {
"amount": 35000,
"currency": "PYG",
"stripePriceId": "price_PY_ID"
}
},
"isActive": true,
"displayOrder": 1,
"badge": { ... } // Opcional
}
]
```
---
## 🎯 Fluxo Recomendado
1. **Desenvolvimento (localhost)**:
```bash
# Editar plans.json com Price IDs de teste
./update-plans.sh
```
2. **Staging**:
```bash
# SSH no servidor staging
ssh user@staging-server
cd /app/qrrapido
./Scripts/update-plans.sh 5000
```
3. **Produção**:
```bash
# Opção 1: Via API (se tiver acesso SSH)
ssh user@prod-server
cd /app/qrrapido
./Scripts/update-plans.sh 5001
# Opção 2: Via MongoDB direto
mongosh "connection_string" < Scripts/seed-mongodb-plans.js
```

View File

@ -0,0 +1,143 @@
# MongoDB Plans Seed Script
## ⚠️ IMPORTANTE - LEIA ANTES DE EXECUTAR
Este script cria **2 planos** no MongoDB:
1. **Premium Mensal** - Cobrança recorrente mensal
2. **Premium Anual** - Cobrança anual com 20% de desconto
**VOCÊ PRECISA ATUALIZAR OS STRIPE PRICE IDs** antes de executar em produção!
## 📋 Pré-requisitos
### 1. Criar Produto no Stripe
1. Acesse [Stripe Dashboard](https://dashboard.stripe.com/products)
2. Crie um produto "QR Rapido Premium"
3. Anote o Product ID (ex: `prod_SnfQTxwE3i8r5L`)
### 2. Criar Price IDs no Stripe
Para **CADA PAÍS** (BR, PY, US), crie **2 preços** (mensal e anual):
#### Brasil (BRL):
- **Mensal**: R$ 9,90/mês
- No Stripe: "Premium Mensal - Brasil"
- Copie o Price ID: `price_xxxxx_monthly_br`
- **Anual**: R$ 95,04/ano (economia de 20%)
- No Stripe: "Premium Anual - Brasil"
- Copie o Price ID: `price_xxxxx_yearly_br`
#### Paraguai (PYG):
- **Mensal**: 35.000 Gs/mês
- No Stripe: "Premium Mensal - Paraguay"
- Copie o Price ID: `price_xxxxx_monthly_py`
- **Anual**: 336.000 Gs/ano (economia de 20%)
- No Stripe: "Premium Anual - Paraguay"
- Copie o Price ID: `price_xxxxx_yearly_py`
#### EUA/Internacional (USD):
- **Mensal**: $1,99/mês
- No Stripe: "Premium Monthly - USA"
- Copie o Price ID: `price_xxxxx_monthly_us`
- **Anual**: $19,10/ano (economia de 20%)
- No Stripe: "Premium Yearly - USA"
- Copie o Price ID: `price_xxxxx_yearly_us`
### 3. Atualizar appsettings.json
**Edite `appsettings.json`** (para desenvolvimento/teste):
```json
"Stripe": {
"Plans": {
"Monthly": {
"BR": "price_xxxxx_monthly_br", // ← Cole seu Price ID aqui
"PY": "price_xxxxx_monthly_py", // ← Cole seu Price ID aqui
"US": "price_xxxxx_monthly_us" // ← Cole seu Price ID aqui
},
"Yearly": {
"BR": "price_xxxxx_yearly_br", // ← Cole seu Price ID aqui
"PY": "price_xxxxx_yearly_py", // ← Cole seu Price ID aqui
"US": "price_xxxxx_yearly_us" // ← Cole seu Price ID aqui
}
}
}
```
**Edite `appsettings.Production.json`** (para produção) com os Price IDs **de produção** (mode live)
## 🚀 Como Executar
### Ambiente Local (Development)
```bash
# Se MongoDB estiver rodando localmente
mongosh "mongodb://localhost:27017/QrRapido" Scripts/seed-mongodb-plans.js
```
### Ambiente de Produção
```bash
# Conectar ao MongoDB de produção e executar o script
mongosh "mongodb://admin:c4rn31r0@129.146.116.218:27017,141.148.162.114:27017/QrRapido?replicaSet=rs0&authSource=admin" Scripts/seed-mongodb-plans.js
```
**OU via SSH no servidor:**
```bash
# Conectar ao servidor
ssh ubuntu@141.148.162.114
# Copiar o script para o servidor
# (você pode usar scp ou copiar o conteúdo manualmente)
# Executar o script
mongosh "mongodb://admin:c4rn31r0@localhost:27017/QrRapido?authSource=admin" seed-mongodb-plans.js
```
## ✅ Verificar se Funcionou
```bash
# Conectar ao MongoDB
mongosh "mongodb://admin:c4rn31r0@129.146.116.218:27017,141.148.162.114:27017/QrRapido?replicaSet=rs0&authSource=admin"
# Verificar os planos inseridos
use QrRapido
db.Plans.find().pretty()
```
Você deve ver um documento com:
- `name`: Nomes em pt-BR, es-PY, en
- `stripePriceId`: O Price ID padrão do Stripe
- `pricesByCountry`: Preços por país (BR, PY, US)
- `isActive`: true
## 🔧 Troubleshooting
### Erro: "Authentication failed"
- Verifique se a senha do MongoDB está correta no connection string
- Verifique se o usuário `admin` tem permissões no banco `QrRapido`
### Erro: "MongoServerError: E11000 duplicate key error"
- Já existe um plano com o mesmo ID
- Execute `db.Plans.deleteMany({})` primeiro para limpar (CUIDADO em produção!)
### Stripe retorna erro "No such price"
- Você não atualizou os `stripePriceId` no script
- Os Price IDs no script não existem no seu Stripe Dashboard
- Verifique se está usando as chaves corretas (test vs live mode)
## 📝 Próximos Passos
Após executar este script:
1. ✅ Verificar no MongoDB que o plano foi criado: `db.Plans.find()`
2. ✅ Testar o fluxo de assinatura em `/Pagamento/SelecaoPlano`
3. ✅ Confirmar que o Stripe recebe o checkout com o Price ID correto
4. ✅ Testar o webhook após pagamento bem-sucedido
## 🔗 Links Úteis
- [Stripe Dashboard - Products](https://dashboard.stripe.com/products)
- [Stripe Dashboard - Prices](https://dashboard.stripe.com/prices)
- [Stripe Webhooks](https://dashboard.stripe.com/webhooks)

View File

@ -0,0 +1,309 @@
#!/bin/bash
# =============================================================================
# Script para criar/recriar Docker Secrets no Swarm
# QR Rapido - Produção
# =============================================================================
#
# USO:
# ./create-docker-secrets.sh # Modo interativo (pede cada valor)
# ./create-docker-secrets.sh --from-env # Lê de variáveis de ambiente
# ./create-docker-secrets.sh --from-file secrets.env # Lê de arquivo .env
# ./create-docker-secrets.sh --delete # Remove todos os secrets
# ./create-docker-secrets.sh --list # Lista secrets existentes
#
# IMPORTANTE: Execute este script no servidor MANAGER do Docker Swarm
# =============================================================================
set -e
# Cores para output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Lista de secrets necessários
SECRETS=(
"stripe_secret_key"
"stripe_webhook_secret"
"mongodb_connection_string"
"google_client_id"
"google_client_secret"
"microsoft_client_id"
"microsoft_client_secret"
)
# Descrições dos secrets
declare -A SECRET_DESCRIPTIONS=(
["stripe_secret_key"]="Stripe API Secret Key (sk_live_xxx)"
["stripe_webhook_secret"]="Stripe Webhook Signing Secret (whsec_xxx)"
["mongodb_connection_string"]="MongoDB Connection String completa"
["google_client_id"]="Google OAuth Client ID"
["google_client_secret"]="Google OAuth Client Secret"
["microsoft_client_id"]="Microsoft OAuth Client ID (GUID)"
["microsoft_client_secret"]="Microsoft OAuth Client Secret"
)
# Função para exibir uso
show_usage() {
echo -e "${BLUE}=== QR Rapido - Docker Secrets Manager ===${NC}"
echo ""
echo "Uso: $0 [opção]"
echo ""
echo "Opções:"
echo " (sem opção) Modo interativo - pede cada valor"
echo " --from-env Lê valores de variáveis de ambiente"
echo " --from-file F Lê valores de arquivo .env"
echo " --delete Remove todos os secrets"
echo " --list Lista secrets existentes"
echo " --check Verifica quais secrets estão faltando"
echo " --help Exibe esta ajuda"
echo ""
echo "Variáveis de ambiente esperadas (para --from-env):"
for secret in "${SECRETS[@]}"; do
local env_var=$(echo "$secret" | tr '[:lower:]' '[:upper:]')
echo " $env_var"
done
}
# Função para verificar se está no Swarm
check_swarm() {
if ! docker info 2>/dev/null | grep -q "Swarm: active"; then
echo -e "${RED}ERRO: Docker Swarm não está ativo neste nó.${NC}"
echo "Execute este script no servidor manager do Swarm."
exit 1
fi
if ! docker info 2>/dev/null | grep -q "Is Manager: true"; then
echo -e "${RED}ERRO: Este nó não é um manager do Swarm.${NC}"
echo "Execute este script no servidor manager do Swarm."
exit 1
fi
}
# Função para listar secrets
list_secrets() {
echo -e "${BLUE}=== Secrets existentes ===${NC}"
docker secret ls
echo ""
echo -e "${BLUE}=== Status dos secrets do QR Rapido ===${NC}"
for secret in "${SECRETS[@]}"; do
if docker secret inspect "$secret" > /dev/null 2>&1; then
echo -e " ${GREEN}${NC} $secret"
else
echo -e " ${RED}${NC} $secret (não existe)"
fi
done
}
# Função para verificar secrets faltantes
check_secrets() {
echo -e "${BLUE}=== Verificando secrets do QR Rapido ===${NC}"
local missing=0
for secret in "${SECRETS[@]}"; do
if docker secret inspect "$secret" > /dev/null 2>&1; then
echo -e " ${GREEN}${NC} $secret"
else
echo -e " ${RED}${NC} $secret - ${SECRET_DESCRIPTIONS[$secret]}"
missing=$((missing + 1))
fi
done
if [ $missing -gt 0 ]; then
echo ""
echo -e "${YELLOW}$missing secret(s) faltando.${NC}"
echo "Execute: $0 (modo interativo) para criar os secrets faltantes."
return 1
else
echo ""
echo -e "${GREEN}Todos os secrets estão configurados!${NC}"
return 0
fi
}
# Função para deletar secrets
delete_secrets() {
echo -e "${YELLOW}=== Removendo secrets do QR Rapido ===${NC}"
echo -e "${RED}ATENÇÃO: Isso vai remover TODOS os secrets listados!${NC}"
read -p "Tem certeza? (digite 'sim' para confirmar): " confirm
if [ "$confirm" != "sim" ]; then
echo "Operação cancelada."
exit 0
fi
for secret in "${SECRETS[@]}"; do
if docker secret inspect "$secret" > /dev/null 2>&1; then
echo -n "Removendo $secret... "
if docker secret rm "$secret" 2>/dev/null; then
echo -e "${GREEN}OK${NC}"
else
echo -e "${RED}ERRO (pode estar em uso por um service)${NC}"
fi
else
echo -e " $secret - ${YELLOW}não existe${NC}"
fi
done
echo ""
echo -e "${YELLOW}IMPORTANTE: Se algum secret estava em uso, você precisa:${NC}"
echo "1. Parar o service: docker service rm qrrapido-prod"
echo "2. Remover os secrets: $0 --delete"
echo "3. Recriar os secrets: $0"
echo "4. Recriar o service (via CI/CD ou manualmente)"
}
# Função para criar um secret
create_secret() {
local name=$1
local value=$2
# Verifica se já existe
if docker secret inspect "$name" > /dev/null 2>&1; then
echo -e " ${YELLOW}!${NC} $name já existe (pulando)"
return 0
fi
# Cria o secret
echo -n " Criando $name... "
if echo -n "$value" | docker secret create "$name" - > /dev/null 2>&1; then
echo -e "${GREEN}OK${NC}"
return 0
else
echo -e "${RED}ERRO${NC}"
return 1
fi
}
# Função para modo interativo
interactive_mode() {
echo -e "${BLUE}=== Modo Interativo ===${NC}"
echo "Você será perguntado sobre cada secret."
echo "Pressione ENTER para pular secrets que já existem."
echo ""
for secret in "${SECRETS[@]}"; do
# Verifica se já existe
if docker secret inspect "$secret" > /dev/null 2>&1; then
echo -e "${GREEN}${NC} $secret já existe"
read -p " Deseja recriar? (s/N): " recreate
if [ "$recreate" != "s" ] && [ "$recreate" != "S" ]; then
continue
fi
echo -n " Removendo secret antigo... "
docker secret rm "$secret" > /dev/null 2>&1 || true
echo "OK"
fi
echo ""
echo -e "${BLUE}$secret${NC}"
echo " Descrição: ${SECRET_DESCRIPTIONS[$secret]}"
# Lê o valor (sem mostrar na tela)
read -s -p " Valor: " value
echo ""
if [ -z "$value" ]; then
echo -e " ${YELLOW}Pulando (valor vazio)${NC}"
continue
fi
create_secret "$secret" "$value"
done
echo ""
echo -e "${GREEN}=== Concluído ===${NC}"
list_secrets
}
# Função para ler de variáveis de ambiente
from_env_mode() {
echo -e "${BLUE}=== Lendo de variáveis de ambiente ===${NC}"
local errors=0
for secret in "${SECRETS[@]}"; do
local env_var=$(echo "$secret" | tr '[:lower:]' '[:upper:]')
local value="${!env_var}"
if [ -z "$value" ]; then
echo -e " ${RED}${NC} $env_var não está definida"
errors=$((errors + 1))
else
create_secret "$secret" "$value"
fi
done
if [ $errors -gt 0 ]; then
echo ""
echo -e "${RED}$errors variável(is) de ambiente não encontrada(s).${NC}"
exit 1
fi
echo ""
echo -e "${GREEN}=== Concluído ===${NC}"
}
# Função para ler de arquivo
from_file_mode() {
local file=$1
if [ ! -f "$file" ]; then
echo -e "${RED}ERRO: Arquivo não encontrado: $file${NC}"
exit 1
fi
echo -e "${BLUE}=== Lendo de arquivo: $file ===${NC}"
# Carrega as variáveis do arquivo
set -a
source "$file"
set +a
# Usa o modo from_env
from_env_mode
}
# =============================================================================
# MAIN
# =============================================================================
# Verifica se está no Swarm (exceto para --help)
if [ "$1" != "--help" ] && [ "$1" != "-h" ]; then
check_swarm
fi
# Processa argumentos
case "$1" in
--help|-h)
show_usage
;;
--list)
list_secrets
;;
--check)
check_secrets
;;
--delete)
delete_secrets
;;
--from-env)
from_env_mode
;;
--from-file)
if [ -z "$2" ]; then
echo -e "${RED}ERRO: Especifique o arquivo .env${NC}"
echo "Uso: $0 --from-file secrets.env"
exit 1
fi
from_file_mode "$2"
;;
"")
interactive_mode
;;
*)
echo -e "${RED}Opção desconhecida: $1${NC}"
show_usage
exit 1
;;
esac

View File

@ -0,0 +1,50 @@
#!/bin/bash
# =============================================================================
# Script para criar Docker Secrets - QR Rapido PRODUÇÃO
# VALORES REAIS - NÃO COMPARTILHAR!
# =============================================================================
#
# Execute no servidor MANAGER do Docker Swarm (141.148.162.114):
# chmod +x create-secrets-prod.sh
# ./create-secrets-prod.sh
#
# =============================================================================
set -e
echo "=== Criando Docker Secrets para QR Rapido ==="
# Stripe
echo "Criando stripe_secret_key..."
echo -n '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' | docker secret create stripe_secret_key - || echo "Já existe"
echo "Criando stripe_webhook_secret..."
echo -n 'whsec_gYri7qNVHc18sREu2eXnjdRzklwRf3hI' | docker secret create stripe_webhook_secret - || echo "Já existe"
# MongoDB
echo "Criando mongodb_connection_string..."
echo -n 'mongodb://admin:c4rn31r0@129.146.116.218:27017,141.148.162.114:27017/QrRapido?replicaSet=rs0&authSource=admin' | docker secret create mongodb_connection_string - || echo "Já existe"
# Google OAuth
echo "Criando google_client_id..."
echo -n '1080447252222-dqjsu999tvrpb69oj5iapckdh9g8rvha.apps.googleusercontent.com' | docker secret create google_client_id - || echo "Já existe"
echo "Criando google_client_secret..."
echo -n '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' | docker secret create google_client_secret - || echo "Já existe"
# Microsoft OAuth
echo "Criando microsoft_client_id..."
echo -n '9bec3835-acdb-4c5a-8668-6b90955c6ad2' | docker secret create microsoft_client_id - || echo "Já existe"
echo "Criando microsoft_client_secret..."
echo -n 'Oe38Q~FsZ3X5ouptAB6oYyX7MXaGUvxXcqT.aaT9' | docker secret create microsoft_client_secret - || echo "Já existe"
echo ""
echo "=== Secrets criados! ==="
echo ""
docker secret ls
echo ""
echo "=== Próximos passos ==="
echo "1. Remova o service atual: docker service rm qrrapido-prod"
echo "2. Faça o deploy (CI/CD ou manual) para recriar com os secrets"

134
Scripts/plans.json Normal file
View File

@ -0,0 +1,134 @@
[
{
"name": {
"pt-BR": "Premium Mensal",
"es-PY": "Premium Mensual",
"en": "Premium Monthly"
},
"description": {
"pt-BR": "Acesso ilimitado a todos os recursos premium - Cobrança mensal",
"es-PY": "Acceso ilimitado a todas las funciones premium - Facturación mensual",
"en": "Unlimited access to all premium features - Monthly billing"
},
"features": {
"pt-BR": [
"QR Codes ilimitados",
"Sem anúncios",
"Customização avançada",
"Suporte para logos",
"Histórico e downloads",
"Suporte prioritário",
"Cancele a qualquer momento"
],
"es-PY": [
"Códigos QR ilimitados",
"Sin anuncios",
"Personalización avanzada",
"Soporte para logos",
"Historial y descargas",
"Soporte prioritario",
"Cancela cuando quieras"
],
"en": [
"Unlimited QR Codes",
"No ads",
"Advanced customization",
"Logo support",
"History and downloads",
"Priority support",
"Cancel anytime"
]
},
"interval": "month",
"stripePriceId": "price_1SJwebB6bFjHQirAloEqXWd6",
"pricesByCountry": {
"BR": {
"amount": 9.90,
"currency": "BRL",
"stripePriceId": "price_1SJwebB6bFjHQirAloEqXWd6"
},
"PY": {
"amount": 35000,
"currency": "PYG",
"stripePriceId": "price_1SK4Y0B6bFjHQirAaxNHxILi"
},
"US": {
"amount": 1.99,
"currency": "USD",
"stripePriceId": "price_XXXXX_monthly_us"
}
},
"isActive": true,
"displayOrder": 1
},
{
"name": {
"pt-BR": "Premium Anual",
"es-PY": "Premium Anual",
"en": "Premium Yearly"
},
"description": {
"pt-BR": "Acesso ilimitado a todos os recursos premium - Economia de 20% no plano anual!",
"es-PY": "Acceso ilimitado a todas las funciones premium - ¡Ahorra 20% con el plan anual!",
"en": "Unlimited access to all premium features - Save 20% with yearly billing!"
},
"features": {
"pt-BR": [
"QR Codes ilimitados",
"Sem anúncios",
"Customização avançada",
"Suporte para logos",
"Histórico e downloads",
"Suporte prioritário",
"💰 Economia de 20%",
"Cobrança anual única"
],
"es-PY": [
"Códigos QR ilimitados",
"Sin anuncios",
"Personalización avanzada",
"Soporte para logos",
"Historial y descargas",
"Soporte prioritario",
"💰 Ahorro del 20%",
"Facturación anual única"
],
"en": [
"Unlimited QR Codes",
"No ads",
"Advanced customization",
"Logo support",
"History and downloads",
"Priority support",
"💰 Save 20%",
"Billed annually"
]
},
"interval": "year",
"stripePriceId": "price_1SK4X7B6bFjHQirAdMtviPw4",
"pricesByCountry": {
"BR": {
"amount": 95.04,
"currency": "BRL",
"stripePriceId": "price_1SK4X7B6bFjHQirAdMtviPw4"
},
"PY": {
"amount": 336000,
"currency": "PYG",
"stripePriceId": "price_1SK4Y0B6bFjHQirAaxNHxILi"
},
"US": {
"amount": 19.10,
"currency": "USD",
"stripePriceId": "price_XXXXX_yearly_us"
}
},
"isActive": true,
"displayOrder": 2,
"badge": {
"pt-BR": "MELHOR VALOR",
"es-PY": "MEJOR VALOR",
"en": "BEST VALUE"
}
}
]

20
Scripts/secrets-prod.env Normal file
View File

@ -0,0 +1,20 @@
# =============================================================================
# SECRETS DE PRODUÇÃO - QR RAPIDO
# BACKUP - NÃO COMPARTILHAR!
# =============================================================================
# Stripe - Produção
STRIPE_SECRET_KEY=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
STRIPE_WEBHOOK_SECRET=whsec_gYri7qNVHc18sREu2eXnjdRzklwRf3hI
STRIPE_PUBLISHABLE_KEY=pk_live_51Rs42SB6bFjHQirAXIhK2fetsfH7MDYWeTT5jiRGYpIS7g5fCCT0XzLK1tIOdxUYXG4gwN4OEAzuVFw9GTmvq7iM00iJmUSZWB
# MongoDB - Produção
MONGODB_CONNECTION_STRING=mongodb://admin:c4rn31r0@129.146.116.218:27017,141.148.162.114:27017/QrRapido?replicaSet=rs0&authSource=admin
# Google OAuth
GOOGLE_CLIENT_ID=1080447252222-dqjsu999tvrpb69oj5iapckdh9g8rvha.apps.googleusercontent.com
GOOGLE_CLIENT_SECRET=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
# Microsoft OAuth
MICROSOFT_CLIENT_ID=9bec3835-acdb-4c5a-8668-6b90955c6ad2
MICROSOFT_CLIENT_SECRET=Oe38Q~FsZ3X5ouptAB6oYyX7MXaGUvxXcqT.aaT9

View File

@ -0,0 +1,28 @@
# =============================================================================
# Template de Secrets para QR Rapido
# =============================================================================
#
# INSTRUÇÕES:
# 1. Copie este arquivo para 'secrets.env' (ou outro nome)
# 2. Preencha os valores abaixo
# 3. Execute: ./create-docker-secrets.sh --from-file secrets.env
# 4. DELETE o arquivo após criar os secrets (contém dados sensíveis!)
#
# NUNCA faça commit de arquivos .env com valores reais!
# =============================================================================
# Stripe - Obter em https://dashboard.stripe.com/apikeys
STRIPE_SECRET_KEY=sk_live_SUBSTITUA_AQUI
STRIPE_WEBHOOK_SECRET=whsec_SUBSTITUA_AQUI
# MongoDB - Connection string completa com usuário e senha
# Formato: mongodb://user:password@host1:port,host2:port/database?options
MONGODB_CONNECTION_STRING=mongodb://admin:SENHA_AQUI@129.146.116.218:27017,141.148.162.114:27017/QrRapido?replicaSet=rs0&authSource=admin
# Google OAuth - Obter em https://console.cloud.google.com/apis/credentials
GOOGLE_CLIENT_ID=SUBSTITUA_AQUI.apps.googleusercontent.com
GOOGLE_CLIENT_SECRET=GOCSPX-SUBSTITUA_AQUI
# Microsoft OAuth - Obter em https://portal.azure.com/#blade/Microsoft_AAD_RegisteredApps
MICROSOFT_CLIENT_ID=SUBSTITUA_AQUI
MICROSOFT_CLIENT_SECRET=SUBSTITUA_AQUI

View File

@ -0,0 +1,163 @@
// MongoDB Seed Script for Plans Collection
// Run this script with: mongosh "mongodb://admin:c4rn31r0@129.146.116.218:27017,141.148.162.114:27017/QrRapido?replicaSet=rs0&authSource=admin" seed-mongodb-plans.js
// Ensure we're using the correct database
db = db.getSiblingDB('QrRapido');
// Clear existing plans (optional - comment out if you want to keep existing plans)
// db.Plans.deleteMany({});
print("\n📦 Seeding Plans Collection...\n");
// 1. PLANO MENSAL
db.Plans.insertOne({
name: {
"pt-BR": "Premium Mensal",
"es-PY": "Premium Mensual",
"en": "Premium Monthly"
},
description: {
"pt-BR": "Acesso ilimitado a todos os recursos premium - Cobrança mensal",
"es-PY": "Acceso ilimitado a todas las funciones premium - Facturación mensual",
"en": "Unlimited access to all premium features - Monthly billing"
},
features: {
"pt-BR": [
"QR Codes ilimitados",
"Sem anúncios",
"QR Codes dinâmicos (editáveis)",
"Estatísticas avançadas",
"Suporte prioritário",
"Acesso à API",
"Cancele a qualquer momento"
],
"es-PY": [
"Códigos QR ilimitados",
"Sin anuncios",
"Códigos QR dinámicos (editables)",
"Estadísticas avanzadas",
"Soporte prioritario",
"Acceso a la API",
"Cancela cuando quieras"
],
"en": [
"Unlimited QR Codes",
"No ads",
"Dynamic QR Codes (editable)",
"Advanced analytics",
"Priority support",
"API access",
"Cancel anytime"
]
},
interval: "month",
stripePriceId: "price_1SJwebB6bFjHQirAloEqXWd6", // Default price (BR)
pricesByCountry: {
"BR": {
amount: 9.90,
currency: "BRL",
stripePriceId: "price_1SJwebB6bFjHQirAloEqXWd6"
},
"PY": {
amount: 35000,
currency: "PYG",
stripePriceId: "price_1SK4Y0B6bFjHQirAaxNHxILi"
},
"US": {
amount: 1.99,
currency: "USD",
stripePriceId: "price_XXXXX_monthly_us" // TODO: Update with real Stripe Price ID
}
},
isActive: true,
displayOrder: 1
});
print("✅ Plano Mensal inserido");
// 2. PLANO ANUAL (com desconto)
db.Plans.insertOne({
name: {
"pt-BR": "Premium Anual",
"es-PY": "Premium Anual",
"en": "Premium Yearly"
},
description: {
"pt-BR": "Acesso ilimitado a todos os recursos premium - Economia de 20% no plano anual!",
"es-PY": "Acceso ilimitado a todas las funciones premium - ¡Ahorra 20% con el plan anual!",
"en": "Unlimited access to all premium features - Save 20% with yearly billing!"
},
features: {
"pt-BR": [
"QR Codes ilimitados",
"Sem anúncios",
"QR Codes dinâmicos (editáveis)",
"Estatísticas avançadas",
"Suporte prioritário",
"Acesso à API",
"💰 Economia de 20%",
"Cobrança anual única"
],
"es-PY": [
"Códigos QR ilimitados",
"Sin anuncios",
"Códigos QR dinámicos (editables)",
"Estadísticas avanzadas",
"Soporte prioritario",
"Acceso a la API",
"💰 Ahorro del 20%",
"Facturación anual única"
],
"en": [
"Unlimited QR Codes",
"No ads",
"Dynamic QR Codes (editable)",
"Advanced analytics",
"Priority support",
"API access",
"💰 Save 20%",
"Billed annually"
]
},
interval: "year",
stripePriceId: "price_1SK4X7B6bFjHQirAdMtviPw4", // Default price (BR)
pricesByCountry: {
"BR": {
amount: 95.04, // 9.90 * 12 * 0.80 = Economia de 20%
currency: "BRL",
stripePriceId: "price_1SK4X7B6bFjHQirAdMtviPw4"
},
"PY": {
amount: 336000, // 35000 * 12 * 0.80 = Economia de 20%
currency: "PYG",
stripePriceId: "price_1SK4Y0B6bFjHQirAaxNHxILi"
},
"US": {
amount: 19.10, // 1.99 * 12 * 0.80 = Economia de 20%
currency: "USD",
stripePriceId: "price_XXXXX_yearly_us" // TODO: Update with real Stripe Price ID
}
},
isActive: true,
displayOrder: 2,
badge: {
"pt-BR": "MELHOR VALOR",
"es-PY": "MEJOR VALOR",
"en": "BEST VALUE"
}
});
print("✅ Plano Anual inserido");
print("\n✅ Plans collection seeded successfully!");
print("\n⚠ PRÓXIMOS PASSOS:");
print("1. Acesse o Stripe Dashboard: https://dashboard.stripe.com/prices");
print("2. Crie os Price IDs para cada país e plano (mensal e anual)");
print("3. Copie os Price IDs (começam com 'price_')");
print("4. Atualize os valores no appsettings.json > Stripe:Plans");
print("5. Execute este script novamente após atualizar os Price IDs acima");
print("\n📊 Verificar planos inseridos:");
print(" db.Plans.find().pretty()");
print("\n🗑 Para limpar e começar de novo:");
print(" db.Plans.deleteMany({})");
print("");

63
Scripts/update-plans.sh Normal file
View File

@ -0,0 +1,63 @@
#!/bin/bash
# Script to update MongoDB Plans via localhost-only API endpoint
# Usage: ./update-plans.sh [port]
PORT=${1:-52428}
API_URL="https://localhost:$PORT/api/Admin/SeedPlans"
PLANS_FILE="$(dirname "$0")/plans.json"
echo "🔧 QR Rapido - Update Plans Script"
echo "=================================="
echo ""
echo "📋 Plans file: $PLANS_FILE"
echo "🌐 API URL: $API_URL"
echo ""
# Check if plans.json exists
if [ ! -f "$PLANS_FILE" ]; then
echo "❌ Error: plans.json not found at $PLANS_FILE"
echo ""
echo "Please create plans.json with your Stripe Price IDs"
exit 1
fi
# Check if the app is running
echo "🔍 Checking if app is running on port $PORT..."
if ! curl -k -s "https://localhost:$PORT/health" > /dev/null 2>&1; then
echo "❌ Error: App not responding on https://localhost:$PORT"
echo ""
echo "Please start the app first:"
echo " dotnet run"
exit 1
fi
echo "✅ App is running"
echo ""
# Send request
echo "📤 Sending plans to API..."
response=$(curl -k -s -w "\n%{http_code}" \
-X POST \
-H "Content-Type: application/json" \
-d @"$PLANS_FILE" \
"$API_URL")
http_code=$(echo "$response" | tail -n1)
body=$(echo "$response" | sed '$d')
echo ""
if [ "$http_code" -eq 200 ]; then
echo "✅ Success! Plans updated in MongoDB"
echo ""
echo "$body" | jq '.' 2>/dev/null || echo "$body"
else
echo "❌ Error! HTTP Status: $http_code"
echo ""
echo "$body"
exit 1
fi
echo ""
echo "🎉 Done!"

View File

@ -29,7 +29,8 @@ namespace QRRapidoApp.Services
var user = await _userService.GetUserAsync(userId);
if (user == null) return true;
return !(user.IsPremium && user.PremiumExpiresAt > DateTime.UtcNow);
// Nova Lógica: Se tem créditos OU ainda tem cota grátis, não mostra anúncios
return !(user.Credits > 0 || user.FreeQRsUsed < 5);
}
catch (Exception ex)
{
@ -43,7 +44,8 @@ namespace QRRapidoApp.Services
try
{
var user = await _userService.GetUserAsync(userId);
return user?.IsPremium == true && user.PremiumExpiresAt > DateTime.UtcNow;
// Nova Lógica: "Premium" visualmente agora significa ter saldo ou cota
return user != null && (user.Credits > 0 || user.FreeQRsUsed < 5);
}
catch (Exception ex)
{
@ -108,5 +110,11 @@ namespace QRRapidoApp.Services
_logger.LogError(ex, $"Error deactivating expired sessions: {ex.Message}");
}
}
public void SetViewBagAds(dynamic viewBag)
{
viewBag.AdSenseTag = _config["AdSense:ClientId"];
viewBag.AdSenseEnabled = bool.TryParse(_config["AdSense:Enabled"], out var enabled) && enabled;
}
}
}
}

View File

@ -0,0 +1,49 @@
using System;
using System.Globalization;
using Microsoft.Extensions.Options;
using QRRapidoApp.Models.Ads;
namespace QRRapidoApp.Services.Ads
{
public class ConfigurationAdSlotProvider : IAdSlotConfigurationProvider
{
private readonly IOptionsSnapshot<AdsConfigurationOptions> _options;
public ConfigurationAdSlotProvider(IOptionsSnapshot<AdsConfigurationOptions> options)
{
_options = options;
}
public AdSlotConfiguration GetSlot(string slotKey, string? cultureName = null)
{
var resolvedCulture = cultureName;
if (string.IsNullOrWhiteSpace(resolvedCulture))
{
resolvedCulture = CultureInfo.CurrentUICulture?.Name;
}
var slot = _options.Value.GetSlot(slotKey, resolvedCulture);
if (string.Equals(slot.Provider, "Affiliate", StringComparison.OrdinalIgnoreCase))
{
if (slot.Affiliate == null || slot.Affiliate.IsEmpty())
{
// Fallback gracefully to AdSense if affiliate content is not properly configured.
return new AdSlotConfiguration
{
Provider = "AdSense",
AdSenseSlotId = slot.AdSenseSlotId
};
}
slot.Provider = "Affiliate"; // normalize casing
}
else
{
slot.Provider = "AdSense";
}
return slot;
}
}
}

View File

@ -0,0 +1,9 @@
using QRRapidoApp.Models.Ads;
namespace QRRapidoApp.Services.Ads
{
public interface IAdSlotConfigurationProvider
{
AdSlotConfiguration GetSlot(string slotKey, string? cultureName = null);
}
}

View File

@ -0,0 +1,88 @@
using Microsoft.Extensions.Caching.Distributed;
using QRRapidoApp.Models;
namespace QRRapidoApp.Services
{
/// <summary>
/// Fixed-window rate limiter backed by IDistributedCache (works with MemoryCache or Redis).
/// Minor race conditions at window boundaries are acceptable for rate limiting purposes.
/// </summary>
public class ApiRateLimitService : IApiRateLimitService
{
private readonly IDistributedCache _cache;
private readonly ILogger<ApiRateLimitService> _logger;
public ApiRateLimitService(IDistributedCache cache, ILogger<ApiRateLimitService> logger)
{
_cache = cache;
_logger = logger;
}
public async Task<RateLimitResult> CheckAndIncrementAsync(string keyPrefix, ApiPlanTier tier)
{
var (perMin, perMonth) = ApiPlanLimits.GetLimits(tier);
var now = DateTime.UtcNow;
var minuteKey = $"rl:min:{keyPrefix}:{now:yyyyMMddHHmm}";
var monthKey = $"rl:mo:{keyPrefix}:{now:yyyyMM}";
var nextMinute = new DateTime(now.Year, now.Month, now.Day, now.Hour, now.Minute, 0, DateTimeKind.Utc)
.AddMinutes(1);
var resetTimestamp = ((DateTimeOffset)nextMinute).ToUnixTimeSeconds();
var minStr = await _cache.GetStringAsync(minuteKey);
var moStr = await _cache.GetStringAsync(monthKey);
int minCount = int.TryParse(minStr, out var mc) ? mc : 0;
int moCount = int.TryParse(moStr, out var mo) ? mo : 0;
bool monthlyExceeded = false;
bool allowed = true;
if (tier != ApiPlanTier.Enterprise)
{
if (moCount >= perMonth)
{
monthlyExceeded = true;
allowed = false;
}
else if (minCount >= perMin)
{
allowed = false;
}
}
if (allowed)
{
var minOpts = new DistributedCacheEntryOptions
{
AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(2)
};
var moOpts = new DistributedCacheEntryOptions
{
AbsoluteExpirationRelativeToNow = TimeSpan.FromDays(33)
};
await Task.WhenAll(
_cache.SetStringAsync(minuteKey, (minCount + 1).ToString(), minOpts),
_cache.SetStringAsync(monthKey, (moCount + 1).ToString(), moOpts)
);
}
int reportedMinLimit = perMin == int.MaxValue ? -1 : perMin;
int reportedMoLimit = perMonth == int.MaxValue ? -1 : perMonth;
int usedMin = minCount + (allowed ? 1 : 0);
int usedMo = moCount + (allowed ? 1 : 0);
return new RateLimitResult(
Allowed: allowed,
PerMinuteLimit: reportedMinLimit,
PerMinuteUsed: usedMin,
ResetUnixTimestamp: resetTimestamp,
MonthlyLimit: reportedMoLimit,
MonthlyUsed: usedMo,
MonthlyExceeded: monthlyExceeded
);
}
}
}

View File

@ -1,126 +0,0 @@
using Microsoft.Extensions.Diagnostics.HealthChecks;
using System.Diagnostics;
using System.Text;
namespace QRRapidoApp.Services.HealthChecks
{
public class SeqHealthCheck : IHealthCheck
{
private readonly IConfiguration _configuration;
private readonly ILogger<SeqHealthCheck> _logger;
private readonly HttpClient _httpClient;
private readonly int _timeoutSeconds;
private readonly string _testLogMessage;
public SeqHealthCheck(
IConfiguration configuration,
ILogger<SeqHealthCheck> logger,
IHttpClientFactory httpClientFactory)
{
_configuration = configuration;
_logger = logger;
_httpClient = httpClientFactory.CreateClient();
_timeoutSeconds = configuration.GetValue<int>("HealthChecks:Seq:TimeoutSeconds", 3);
_testLogMessage = configuration.GetValue<string>("HealthChecks:Seq:TestLogMessage", "QRRapido health check test");
}
public async Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default)
{
var stopwatch = Stopwatch.StartNew();
var data = new Dictionary<string, object>();
var seqUrl = _configuration["Serilog:SeqUrl"];
if (string.IsNullOrEmpty(seqUrl))
{
return HealthCheckResult.Degraded("Seq URL not configured - logging to console only", data: data);
}
try
{
using var timeoutCts = new CancellationTokenSource(TimeSpan.FromSeconds(_timeoutSeconds));
using var combinedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeoutCts.Token);
// Test basic connectivity to Seq server
var pingUrl = $"{seqUrl.TrimEnd('/')}/api";
var response = await _httpClient.GetAsync(pingUrl, combinedCts.Token);
var latencyMs = stopwatch.ElapsedMilliseconds;
data["reachable"] = response.IsSuccessStatusCode;
data["latency"] = $"{latencyMs}ms";
data["seqUrl"] = seqUrl;
data["statusCode"] = (int)response.StatusCode;
if (!response.IsSuccessStatusCode)
{
data["error"] = $"HTTP {response.StatusCode}";
return HealthCheckResult.Unhealthy($"Seq server not reachable at {seqUrl} (HTTP {response.StatusCode})", data: data);
}
// Try to send a test log message if we can access the raw events endpoint
try
{
await SendTestLogAsync(seqUrl, combinedCts.Token);
data["lastLog"] = DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ssZ");
data["testLogSent"] = true;
}
catch (Exception logEx)
{
_logger.LogWarning(logEx, "Failed to send test log to Seq during health check");
data["testLogSent"] = false;
data["testLogError"] = logEx.Message;
}
stopwatch.Stop();
data["totalCheckTimeMs"] = stopwatch.ElapsedMilliseconds;
// Determine health status
if (latencyMs > 2000)
{
return HealthCheckResult.Degraded($"Seq responding slowly ({latencyMs}ms)", data: data);
}
return HealthCheckResult.Healthy($"Seq healthy ({latencyMs}ms)", data: data);
}
catch (OperationCanceledException)
{
data["reachable"] = false;
data["error"] = "timeout";
return HealthCheckResult.Unhealthy($"Seq health check timed out after {_timeoutSeconds} seconds", data: data);
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Seq health check failed");
data["reachable"] = false;
data["error"] = ex.Message;
return HealthCheckResult.Unhealthy($"Seq health check failed: {ex.Message}", data: data);
}
}
private async Task SendTestLogAsync(string seqUrl, CancellationToken cancellationToken)
{
var apiKey = _configuration["Serilog:ApiKey"];
var eventsUrl = $"{seqUrl.TrimEnd('/')}/api/events/raw";
// Create a simple CLEF (Compact Log Event Format) message
var timestamp = DateTimeOffset.UtcNow.ToString("yyyy-MM-ddTHH:mm:ss.fffK");
var logEntry = $"{{\"@t\":\"{timestamp}\",\"@l\":\"Information\",\"@m\":\"Health check test from QRRapido\",\"ApplicationName\":\"QRRapido\",\"HealthCheck\":true,\"TestMessage\":\"{_testLogMessage}\"}}";
var content = new StringContent(logEntry, Encoding.UTF8, "application/vnd.serilog.clef");
// Add API key if configured
if (!string.IsNullOrEmpty(apiKey))
{
content.Headers.Add("X-Seq-ApiKey", apiKey);
}
var response = await _httpClient.PostAsync(eventsUrl, content, cancellationToken);
if (!response.IsSuccessStatusCode)
{
throw new HttpRequestException($"Failed to send test log to Seq: HTTP {response.StatusCode}");
}
}
}
}

View File

@ -0,0 +1,23 @@
using QRRapidoApp.Models;
namespace QRRapidoApp.Services
{
public record RateLimitResult(
bool Allowed,
int PerMinuteLimit,
int PerMinuteUsed,
long ResetUnixTimestamp,
int MonthlyLimit,
int MonthlyUsed,
bool MonthlyExceeded
);
public interface IApiRateLimitService
{
/// <summary>
/// Checks the rate limit for the given API key prefix and plan tier.
/// Increments the counter only if the request is allowed.
/// </summary>
Task<RateLimitResult> CheckAndIncrementAsync(string keyPrefix, ApiPlanTier tier);
}
}

Some files were not shown because too many files have changed in this diff Show More