Skip to content

Repository files navigation

FinControl

Aplicativo de controle de orçamento pessoal com rendas, despesas mensais, despesas futuras, rendas futuras, investimentos e suporte multi-moeda (BRL / USD / EUR). Inclui gráficos, indicadores de saúde financeira, projeção de despesas futuras, histórico mensal e widget de cotação de câmbio.

Desenvolvido por Pedro Henrique de Almeida.

🌐 Em produção

A aplicação está rodando em:

https://fincontroltech.up.railway.app

Stack: Python 3.11 + Flask + PostgreSQL (Neon), deploy em Railway (Docker). Back e front servidos pelo mesmo processo (mesma origem, sem CORS aberto).

✨ Funcionalidades

  • Autenticação: cadastro e login com usuário/e-mail e senha (hash PBKDF2 + JWT).
  • Multi-moeda: registre rendas, despesas e investimentos em BRL, USD ou EUR. O sistema converte tudo automaticamente para BRL no saldo e no histórico mensal.
  • Widget de Câmbio no dashboard: mostra USD/BRL e EUR/BRL com timestamp e botão de refresh. Modo manual como fallback se a API estiver bloqueada.
  • Rendas ativas e passivas com categoria (Salário, Freelance, Aluguel…).
  • Rendas futuras (13º, PLR, restituição de IR…) com prazo em meses.
  • Despesas mensais com dia de vencimento e categoria.
  • Despesas futuras com prazo em meses (ex.: IPTU daqui a 6 meses).
  • Investimentos com rendimento anual (% a.a.) e cálculo de rendimento mensal.
  • Histórico mensal: salve um snapshot dos totais a cada mês e acompanhe a evolução da renda, despesas e saldo em um gráfico de linha.
  • Gráficos: pizza de distribuição de despesas + barra renda × despesas + linha de evolução temporal.
  • Saúde financeira: taxa de poupança, reserva de emergência (em meses), comprometimento da renda e classificação geral (Excelente → Crítico).
  • Projeção de despesas futuras com sugestão de reserva mensal.
  • Dados isolados por usuário — ninguém vê os dados de outra pessoa.
  • UI responsiva com Tailwind via CDN (sem build step).

🧱 Stack

Camada Tecnologia
Front-end HTML + CSS + JS (vanilla, Tailwind/Chart.js CDN)
Back-end Python 3.11 / Flask 3 + flask-cors + flask-limiter
Banco PostgreSQL (Neon) em prod · SQLite em dev
Auth JWT (PyJWT) + PBKDF2-SHA256 para hash de senha
Adapter psycopg[binary]>=3.1 (Postgres) — adapter dual-backend

Nenhuma etapa de build. pip install + python app.py e está rodando.


💰 Reserva de Emergência

Cada usuário tem uma reserva pessoal com:

  • Meta em meses de despesas (padrão: 6, ajustável de 1 a 24)
  • Valor atual (soma de contribuições - retiradas)
  • Progresso visual com barra de cor (vermelho <30%, amarelo 30-70%, verde ≥70%)
  • Recomendação mensal pra atingir a meta em 1 ano
  • Contribuir / Retirar com nota (ex: "Salário outubro", "Dentista")
  • Histórico das últimas 20 movimentações

Endpoints

Método Rota Descrição
GET /api/emergency-reserve Estado + métricas calculadas
PUT /api/emergency-reserve Atualiza meta (target_months) + nota
POST /api/emergency-reserve/contribute Adiciona dinheiro
POST /api/emergency-reserve/withdraw Retira (emergência)
GET /api/emergency-reserve/transactions Histórico (limit 1-100)

Cálculos

target_amount   = monthly_expenses × target_months
progress_pct    = min(100, current / target_amount × 100)
recommended     = max(0, (target_amount - current) / 12)  # 1 ano
is_complete     = current >= target_amount (e target > 0)

Validações

  • target_months: 1-24
  • contribution: ≥ 0
  • amount: > 0, ≤ R$ 1.000.000
  • withdraw: amount ≤ current_amount
  • notes: max 500 chars (settings) / 200 chars (tx)

Schema

emergency_reserve (
    id, user_id UNIQUE,
    current_amount, target_months, monthly_contribution,
    notes, updated_at
)

reserve_transactions (
    id, user_id, amount, kind,        -- 'contribute' ou 'withdraw'
    note, created_at
)

💱 Câmbio multi-moeda

Fontes de cotação (em ordem de preferência)

  1. CurrencyAPI (via jsdelivr CDN) — primária, sem rate limit, sem API key
  2. Banco Central do Brasil (BCB) — oficial, fallback
  3. Frankfurter (BCE) — fallback final

Todas as fontes são cacheadas por 24h. Refresh "lazy" — primeira request após 24h dispara nova busca.

⚠️ Limitação em deploys Railway / containers Docker

Em alguns ambientes (como o Railway free tier), o outbound HTTP é bloqueado por padrão para domínios externos. Isso afeta as 3 fontes acima, e o botão de refresh vai mostrar a mensagem "Não foi possível buscar cotações agora".

Solução implementada: modo manual de cotação.

  • Botão ✏️ no widget Câmbio (ao lado do 🔄 de refresh)
  • Form com 2 inputs (USD e EUR) + botão Salvar
  • Você digita os valores (Google, BCB, Wise — qualquer fonte)
  • Salva no cache; o widget mostra Atualizado: ... (manual)
  • O cálculo de saldo e conversão passam a usar esses valores

Histórico de mudanças da fonte

Período Fonte usada Motivo da mudança
v1.x AwesomeAPI (economia.awesomeapi.com.br) 429 Quota Exceeded (mudaram política, exige API key)
v1.x BCB + Frankfurter Funcionavam local; Railway bloqueava outbound
v1.x+ CurrencyAPI (jsdelivr) + modo manual jsdelivr também bloqueado no Railway; modo manual cobre

Conversão automática

Todos os totais em BRL:

  • Saldo no dashboard
  • Saldos do histórico mensal
  • Soma de investimentos (cálculo de rendimento mensal)
  • Projeção de despesas

Itens em USD/EUR mostram o valor original + valor BRL convertido embaixo.


📱 PWA (Progressive Web App)

O app é instalável como app nativo no celular/desktop. Funciona offline com cache, splash screen e ícone próprio.

Como instalar

Android (Chrome/Edge)

  1. Abra o app no navegador
  2. Banner aparece: "Adicionar à tela inicial" — ou clique no botão Instalar (verde) no header
  3. O app fica na home screen com o ícone R$

iOS (Safari)

  1. Abra o app
  2. Botão de compartilhar (quadrado com seta) → "Adicionar à tela de início"
  3. Confirme o nome "FinControl"

Desktop (Chrome/Edge)

  1. Ícone de instalação na barra de URL
  2. Ou menu → "Instalar FinControl"
  3. Abre em janela própria (sem barra de URL)

Recursos PWA

  • Instalável (home screen) com ícone R$ verde
  • Splash screen com gradiente verde + logo
  • Funciona offline: cache do shell + última versão dos CDNs
  • Página offline customizada se o SW não tiver cache
  • Shortcuts no launcher (Android): Nova renda, Nova despesa, Câmbio
  • Auto-update: prompt de nova versão + auto-reload 8s
  • Theme color combinando com o app (#10b981)
  • Status bar translúcida (iOS) combinando com o app

Estrutura PWA

manifest.json   # Metadados (name, icons, shortcuts, theme_color)
sw.js           # Service Worker (cache + offline)
offline.html    # Fallback quando sem rede e sem cache
icons/
  icon-192.png  # Home screen (Android/iOS)
  icon-512.png  # Splash screen
  icon-192.svg  # Vetor (fallback + maskable)

Estratégia de cache do Service Worker

Recurso Estratégia Motivo
API (/api/*) network-first → JSON "offline" Sempre tenta fresh; offline falha gracefully
CDNs stale-while-revalidate Carrega do cache, atualiza em background
Front-end network-first → cache → offline.html Fresh quando possível, fallback robusto
POST/PUT/DELETE network-only (não intercepta) Mutations não podem ser cacheadas

⚠️ Importante no deploy

O Dockerfile precisa incluir os arquivos PWA no build. Certifique-se que tem:

COPY manifest.json sw.js offline.html /app/
COPY icons/ /app/icons/

Sem isso, o SW não registra, manifest falha com 404, e o app não fica instalável. (Já está corrigido no Dockerfile atual — vale checar depois de qualquer mudança na estrutura.)


📂 Estrutura

Fin_Control/
├── index.html             # App principal (protegido)
├── login.html             # Tela de login
├── register.html          # Tela de cadastro
├── manifest.json          # PWA manifest (name, icons, shortcuts)
├── sw.js                  # Service Worker (cache + offline)
├── offline.html           # Fallback quando sem rede
├── style.css              # Estilos (inclui tema das telas de auth)
├── script.js              # Lógica do app (consome a API)
├── api.js                 # Cliente HTTP + storage do token JWT
├── auth.js                # Comportamento das telas de login/registro
├── icons/                 # Ícones PWA
│   ├── icon-192.png
│   ├── icon-512.png
│   └── icon-192.svg
├── render.yaml            # Deploy 1-clique no Render.com
├── security_audit.py      # Auditoria automatizada (roda contra a URL em produção)
├── backend/
│   ├── app.py             # Entry point Flask + hardening (CORS, headers)
│   ├── rate_limit.py      # Flask-Limiter compartilhado
│   ├── database.py        # Schema dual-backend (SQLite/Postgres) + helpers
│   ├── auth.py            # Hash PBKDF2 + JWT + decorator @auth_required
│   ├── auth_routes.py     # /api/auth/register, /login, /me, /logout
│   ├── data_routes.py     # /api/data + CRUD + /api/history + /api/exchange-rates
│   ├── exchange_rates.py  # Módulo de cotação (3 fontes + cache 24h + manual)
│   ├── test_api.py        # Smoke test do backend
│   ├── test_e2e.py        # Fluxo end-to-end
│   ├── test_new_features.py  # future_incomes + monthly_history
│   ├── test_currency.py   # Testes do multi-moeda
│   ├── test_bcb.py        # Testes do fetch de cotação (BCB + manual)
│   ├── test_psycopg_compat.py  # Compat psycopg 3 (DELETE/UPDATE/INSERT)
│   ├── requirements.txt
│   ├── Dockerfile         # Imagem de produção (gunicorn) — inclui arquivos PWA
│   ├── Procfile           # Heroku-style start command
│   ├── runtime.txt        # Versão do Python
│   └── .gitignore
└── README.md

🚀 Como rodar

1. Instalar dependências do back-end

cd backend
python3 -m venv .venv
source .venv/bin/activate       # Windows: .venv\Scripts\activate
pip install -r requirements.txt

2. Subir o servidor (SQLite local)

python app.py

O servidor escuta em http://127.0.0.1:5000 por padrão. Para mudar: PORT=8080 python app.py. O backend detecta automaticamente: sem DATABASE_URL → SQLite, com DATABASE_URL → Postgres.

3. Abrir o front-end

4. (Opcional) Configurações de ambiente

Variável Padrão Descrição
PORT 5000 Porta HTTP
FLASK_DEBUG 0 1 para modo debug do Flask
JWT_SECRET dev-secret-change-me-in-production Chave HMAC dos tokens (troque em prod!)
DATABASE_URL (vazio) Se começar com postgres:// ou postgresql:// → usa Postgres
FINCONTROL_DB backend/fincontrol.db Caminho do arquivo SQLite (ignorado se DATABASE_URL setado)
ALLOWED_ORIGINS https://fincontrol.pythonanywhere.com Origens CORS (vírgula)
DISABLE_RATE_LIMIT (vazio) 1 para desabilitar rate limit (só em testes)
DISABLE_EXCHANGE_FETCH (vazio) 1 para desabilitar fetch de cotação (só em testes)

🔐 API

Todas as rotas de dados exigem o header Authorization: Bearer <token>. Respostas em JSON. Erros vêm como {"error": "mensagem"}.

Autenticação

Método Rota Body Resposta
POST /api/auth/register {username, email, password} 201 {token, user}
POST /api/auth/login {username, password} ou {email, password} 200 {token, user}
GET /api/auth/me 200 {user}
POST /api/auth/logout 200 {ok: true}

Dados do usuário

Método Rota Descrição
GET /api/data Snapshot completo (inclui exchangeRates)
GET /api/<resource> Lista itens
POST /api/<resource> Cria item
PUT /api/<resource>/<id> Atualiza item
DELETE /api/<resource>/<id> Remove item

Recursos: incomes, passive_incomes, monthly_expenses, future_expenses, future_incomes, investments.

Body dos itens (multi-moeda)

Todos os recursos aceitam o campo opcional currency ("BRL", "USD", "EUR"). Default: "BRL".

// POST /api/monthly_expenses
{
  "name": "Aluguel",
  "value": 2200,
  "category": "Moradia",
  "currency": "BRL",
  "due_day": 5
}

// POST /api/incomes  (em USD)
{
  "name": "Freela USD",
  "value": 1000,
  "category": "Freelance",
  "currency": "USD"
}

Histórico mensal

Método Rota Descrição
GET /api/history Lista snapshots salvos (mais recente primeiro)
POST /api/history/snapshot Salva/atualiza um snapshot (recalcula totais se omitidos, convertendo para BRL)
PUT /api/history/<id> Atualiza apenas as notas de um snapshot
DELETE /api/history/<id> Remove um snapshot

Exemplos:

// POST /api/history/snapshot
{
  "month_year": "2026-07",        // opcional: padrão = mês atual
  "notes": "Mês de férias"         // opcional
}

Câmbio

Método Rota Descrição
GET /api/exchange-rates Retorna cotações cacheadas (BRL/1, USD, EUR)
POST /api/exchange-rates/refresh Força refresh do cache (tenta APIs externas)
POST /api/exchange-rates/manual Define cotações manualmente (fallback)

Exemplo de body para cotação manual:

// POST /api/exchange-rates/manual
{ "USD": 5.07, "EUR": 5.85 }

Health

Método Rota Descrição
GET /api/health Status do serviço + info do DB

🗄️ Banco de dados

Adapter dual-backend

O database.py detecta automaticamente o backend pela env DATABASE_URL:

  • postgres:// ou postgresql://psycopg 3
  • caso contrário → SQLite (arquivo local)

Os placeholders ? são adaptados para %s quando o backend é Postgres, mantendo o mesmo código SQL nas queries.

Schema

Tabelas principais:

  • users — contas
  • incomes / passive_incomes — rendas
  • monthly_expenses / future_expenses / future_incomes — despesas e rendas
  • investments — investimentos
  • monthly_history — snapshots mensais (totais já convertidos pra BRL)
  • exchange_rates — cache de cotações (atualizado diariamente)

Todas as tabelas financeiras têm coluna currency (BRL/USD/EUR). A coluna exchange_rates.code é TEXT UNIQUE; tem também um id próprio.

Migrações

Idempotentes: rodam em todo boot do app, sem efeito colateral:

  • CREATE TABLE IF NOT EXISTS para todas
  • ALTER TABLE ... ADD COLUMN IF NOT EXISTS (Postgres) ou PRAGMA table_info (SQLite)
  • Auto-repair: tabela exchange_rates é DROP+CREATE se schema quebrado

🧪 Testes

Seis scripts prontos, todos usam apenas a stdlib (urllib):

cd backend
python3 test_api.py                  # ~20 cenários: auth, validações, isolamento
python3 test_e2e.py                  # fluxo UI-equivalente completo
python3 test_new_features.py         # future_incomes + monthly_history
python3 test_currency.py             # multi-moeda (validação + conversão)
python3 test_bcb.py                  # fetch de cotação (BCB + manual)
python3 test_psycopg_compat.py       # compat psycopg 3 (DELETE/UPDATE/INSERT)

# Pra rodar tudo
for t in test_*.py; do python3 "$t"; done

Recomendado usar DISABLE_RATE_LIMIT=1 ao rodar a suíte (a não ser que queira testar rate limit também).


🔒 Segurança

O app foi endurecido para uso público. Medidas implementadas:

Criptografia e autenticação

  • Senhas com PBKDF2-HMAC-SHA256, 200.000 iterações, salt aleatório de 16 bytes.
  • JWT com HS256 e expiração de 7 dias.
  • Senha: 6–128 caracteres; usuário: 3-32 chars (a-z A-Z 0-9 _ . -).

Hardening HTTP (em toda resposta)

  • X-Content-Type-Options: nosniff
  • X-Frame-Options: DENY (anti-clickjacking)
  • Strict-Transport-Security: max-age=31536000; includeSubDomains (HSTS)
  • Content-Security-Policy (CSP) restritivo, liberando só os CDNs usados pelo front
  • Referrer-Policy: strict-origin-when-cross-origin
  • Permissions-Policy desabilitando geolocation, câmera, microfone, etc.
  • Server header reescrito para FinControl (esconde stack)
  • Error handlers globais (404/405/500/Exception) retornam JSON com stacktrace nos logs

CORS

  • Origens explícitas e restritas (configurável via ALLOWED_ORIGINS).
  • Default: https://fincontrol.pythonanywhere.com.
  • Em dev, defina ALLOWED_ORIGINS=http://localhost:5000,http://127.0.0.1:5000.

Rate limiting

  • /api/auth/login e /api/auth/register: 10 req/min e 30 req/hora por IP via Flask-Limiter.
  • Resposta 429 Too Many Requests quando excede.

Isolamento de dados

  • Cada query SQL filtra por user_id do JWT — um usuário nunca vê dados de outro.
  • Todos os endpoints de dados exigem Authorization: Bearer <token>; sem ele → 401.

Validação de entrada

  • Username regex, email regex, senha com limites.
  • Currency normalizada pra UPPERCASE e validada ∈ {BRL, USD, EUR}.
  • Payloads grandes / strings longas são rejeitados com 400.
  • Queries 100% parametrizadas (sem concatenação) — imune a SQL Injection clássico.
  • Tokens alg=none e inválidos são rejeitados com 401.

Auditoria

  • security_audit.py (na raiz do repo) faz 80+ verificações automatizadas contra uma URL alvo.

Configuração obrigatória em produção

  • JWT_SECRET: gere um valor forte com python -c "import secrets; print(secrets.token_hex(32))". O default dev-secret-change-me-in-production é apenas para dev local.
  • ALLOWED_ORIGINS: defina o(s) domínio(s) do app, separados por vírgula.
  • DATABASE_URL: connection string do Postgres/Neon em produção.

🚢 Deploy

Railway + Neon Postgres

O Dockerfile está pronto pra Railway. O build context é a raiz do repo (para incluir o front-end), e o CMD usa gunicorn em shell form para interpretar $PORT.

Variáveis de ambiente obrigatórias no Railway:

DATABASE_URL=postgresql://user:pass@host/db?sslmode=require
JWT_SECRET=$(python -c "import secrets; print(secrets.token_hex(32))")
ALLOWED_ORIGINS=https://seu-app.up.railway.app

O init_db() roda em todo boot, criando/migrando as tabelas.

About

Aplicação Web com armazenamento em local storage para controle de finanças pessoais.

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages