Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
37134b6
feat: Refactor Dockerfile, update CI pipeline, and enhance test suite…
Vitorhleme Oct 23, 2025
52a4f80
add master
Vitorhleme Oct 23, 2025
2743ea7
fix(ci): Corrige sintaxe da lista de branches no workflow
Vitorhleme Oct 23, 2025
8c71958
fix: Resolve Ruff linting errors reported by CI
Vitorhleme Oct 23, 2025
5ec5fc8
fix(deps): Import missing security module
Vitorhleme Oct 23, 2025
b7107e0
style: Format codebase with Ruff
Vitorhleme Oct 23, 2025
b708940
Refactor codebase for improved type hinting and error handling
Vitorhleme Oct 23, 2025
9d74e0b
fix: Replace '== False' with 'not' for improved readability in queries
Vitorhleme Oct 23, 2025
5b40913
Reformat
Vitorhleme Oct 23, 2025
cb5421d
fix: Update SQLAlchemy queries to use '== False' for compatibility an…
Vitorhleme Oct 23, 2025
b8dffb4
Fix and reformatted
Vitorhleme Oct 23, 2025
4bf88f5
arrumado erros de [attr-defined] em rowcount
Vitorhleme Oct 23, 2025
7babcbc
Reformat
Vitorhleme Oct 23, 2025
58c81a1
Reformat
Vitorhleme Oct 23, 2025
9b5fcd7
Reformat
Vitorhleme Oct 23, 2025
b7c9703
fix requirements vulnerabilities and ECDSA
Vitorhleme Oct 23, 2025
34085a2
add safety_api_key
Vitorhleme Oct 23, 2025
be7ed83
fix safety scan error
Vitorhleme Oct 23, 2025
adf18d3
fix safety scan
Vitorhleme Oct 23, 2025
d86a530
fix PYTHONPATH
Vitorhleme Oct 23, 2025
1b7873a
fix import
Vitorhleme Oct 23, 2025
5167033
Run Pytest with Coverage
Vitorhleme Oct 23, 2025
398a1ab
Adicionar a linha httpx[fastapi] ao arquivo requirements-dev.txt
Vitorhleme Oct 23, 2025
ab555f4
fix
Vitorhleme Oct 23, 2025
68eac9d
Reformat
Vitorhleme Oct 23, 2025
5e853db
Reformat
Vitorhleme Oct 23, 2025
ed5fcaf
Reformat
Vitorhleme Oct 23, 2025
0115e7e
fix async_client
Vitorhleme Oct 23, 2025
37f6fa8
Reformat
Vitorhleme Oct 23, 2025
fd2b5ff
Add more tests
Vitorhleme Oct 23, 2025
0fca695
Reformat
Vitorhleme Oct 23, 2025
3c7f5c0
Reformat
Vitorhleme Oct 23, 2025
9b74c92
Reformat
Vitorhleme Oct 23, 2025
8e91545
tests
Vitorhleme Oct 24, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added .coverage
Binary file not shown.
83 changes: 83 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
# .github/workflows/ci.yml
#
# Pipeline de CI (Continuous Integration) para a API de Autenticação Python
# Versão Profissional com Múltiplas Verificações

name: Python CI Pipeline

on:
push:
branches: [ "main", "master" ]
pull_request:
branches: [ "main", "master" ]

jobs:
build-and-test:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.10"]

steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}

- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
pip install -r requirements-dev.txt

- name: Lint with Ruff
run: |
ruff check .
ruff format --check .

# --- NOVAS ETAPAS DE VERIFICAÇÃO ---

- name: Static Type Checking (MyPy)
run: |
# Verifica o código da aplicação por erros de tipo
mypy app

- name: Code Security Scan (Bandit)
run: |
# Roda o scan de segurança no código da aplicação (-r: recursivo)
# -lll: Reporta apenas problemas de confiança ALTA (High)
bandit -r app -lll

- name: Dependency Security Scan (Safety)
env:
SAFETY_API_KEY: ${{ secrets.SAFETY_API_KEY }}
run: |
# Usa 'safety check' (em vez de scan) com os flags '-i'
# 'safety check' respeita os flags de ignore locais.
safety check -r requirements.txt -i 64459 -i 64396

# --- ETAPA DE TESTE ATUALIZADA ---

- name: Run Pytest with Coverage
env:
# Corrige o "Module not found"
PYTHONPATH: .

# Variáveis dummy para o config.py
DATABASE_URL: "sqlite+aiosqlite:///./test.db"
SECRET_KEY: "dummy_test_key"
REFRESH_SECRET_KEY: "dummy_test_refresh_key"
SENDGRID_API_KEY: "dummy_sendgrid"
EMAIL_FROM: "test@example.com"
INTERNAL_API_KEY: "dummy_internal_key"

# --- ADICIONE ESTA LINHA ---
RUNNING_TESTS: "true" # Desativa o SlowAPI

run: |
# Roda os testes, medindo a cobertura do diretório 'app'
# Falha o build se a cobertura for menor que 80%
pytest --cov=app --cov-report=term --cov-fail-under=80
32 changes: 23 additions & 9 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,27 +1,41 @@
# ---- Builder Stage ----
# (Esta parte permanece a mesma)
FROM python:3.10-slim AS builder

WORKDIR /app

# Install build dependencies
# Instala apenas as dependências de build (se houver) e de produção
COPY requirements.txt .
COPY requirements-dev.txt .
RUN pip install --no-cache-dir -r requirements.txt
RUN pip install --no-cache-dir -r requirements-dev.txt

# Copy application code
# Copia o código da aplicação
COPY . .

# ---- Final Stage ----
# ---- Final Stage (Otimizado para Produção) ----
FROM python:3.10-slim

WORKDIR /app

# Copy installed dependencies from builder stage
# 1. Criar um usuário não-root para segurança
RUN addgroup --system app && adduser --system --group app

# 2. Copiar apenas os arquivos necessários do builder
# Copia as dependências instaladas
COPY --from=builder /usr/local/lib/python3.10/site-packages /usr/local/lib/python3.10/site-packages
COPY --from=builder /usr/local/bin /usr/local/bin
# Copia a aplicação
COPY --from=builder /app .

# Copy application code
COPY . .
# 3. Definir permissões
RUN chown -R app:app /app

# 4. Mudar para o usuário não-root
USER app

EXPOSE 8001

# 5. Comando de Produção (Gunicorn + Uvicorn)
# Substitui o "uvicorn --reload" do docker-compose
# -w 4: Inicia 4 processos "workers" (ajuste conforme os CPUs do seu servidor)
# -k uvicorn.workers.UvicornWorker: Usa uvicorn como a classe de worker
# --bind 0.0.0.0:8001: Expõe na porta 8001
CMD ["gunicorn", "main:app", "-w", "4", "-k", "uvicorn.workers.UvicornWorker", "--bind", "0.0.0.0:8001"]
24 changes: 13 additions & 11 deletions alembic/env.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,19 @@

# --- 1. Importar Base e Modelos ---
# Adicione sys.path para que o alembic encontre sua pasta 'app'
import os
# import os # <-- REMOVIDO
import sys
from pathlib import Path

# Sobe dois níveis (alembic/ -> raiz) e adiciona ao path
sys.path.append(str(Path(__file__).resolve().parent.parent))

from app.db.base import Base
from app.models import user # noqa F401
from app.models import refresh_token # noqa F401
from app.models import user # noqa F401
from app.models import refresh_token # noqa F401

# --- ADICIONAR NOVO MODELO ---
from app.models import mfa_recovery_code # noqa F401
from app.models import mfa_recovery_code # noqa F401
# --- FIM ADIÇÃO ---
# --- Fim Importar Modelos ---

Expand All @@ -36,6 +38,8 @@

# --- 3. Definir o sqlalchemy.url dinamicamente ---
db_url = settings.DATABASE_URL
if not db_url:
raise ValueError("DATABASE_URL não está definida nas configurações.")
config.set_main_option("sqlalchemy.url", db_url)
# --- Fim MODIFICAÇÃO ---

Expand All @@ -45,7 +49,7 @@
fileConfig(config.config_file_name)

# add your model's MetaData object here
target_metadata = Base.metadata # --- 4. Apontar para a Base do nosso app ---
target_metadata = Base.metadata # --- 4. Apontar para a Base do nosso app ---


def run_migrations_offline() -> None:
Expand All @@ -56,7 +60,7 @@ def run_migrations_offline() -> None:
target_metadata=target_metadata,
literal_binds=True,
dialect_opts={"paramstyle": "named"},
compare_type=True
compare_type=True,
)

with context.begin_transaction():
Expand All @@ -65,9 +69,7 @@ def run_migrations_offline() -> None:

def do_run_migrations(connection: Connection) -> None:
context.configure(
connection=connection,
target_metadata=target_metadata,
compare_type=True
connection=connection, target_metadata=target_metadata, compare_type=True
)

with context.begin_transaction():
Expand All @@ -76,7 +78,7 @@ def do_run_migrations(connection: Connection) -> None:

async def run_migrations_online() -> None:
"""Run migrations in 'online' mode."""

# --- 5. Configuração Assíncrona ---
connectable = create_async_engine(
config.get_main_option("sqlalchemy.url"),
Expand All @@ -95,4 +97,4 @@ async def run_migrations_online() -> None:
else:
# --- 6. Rodar no loop de eventos asyncio ---
asyncio.run(run_migrations_online())
# --- Fim asyncio ---
# --- Fim asyncio ---
46 changes: 30 additions & 16 deletions alembic/versions/7c84b59f63f7_adiciona_tabela_mfa_recovery_codes.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,41 +5,55 @@
Create Date: 2025-10-23 16:07:39.710029

"""

from typing import Sequence, Union

from alembic import op
import sqlalchemy as sa


# revision identifiers, used by Alembic.
revision: str = '7c84b59f63f7'
down_revision: Union[str, Sequence[str], None] = 'cc0065610539'
revision: str = "7c84b59f63f7"
down_revision: Union[str, Sequence[str], None] = "cc0065610539"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None


def upgrade() -> None:
"""Upgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('mfa_recovery_codes',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('user_id', sa.Integer(), nullable=False),
sa.Column('hashed_code', sa.String(length=255), nullable=False),
sa.Column('is_used', sa.Boolean(), nullable=False),
sa.ForeignKeyConstraint(['user_id'], ['users.id'], ),
sa.PrimaryKeyConstraint('id')
op.create_table(
"mfa_recovery_codes",
sa.Column("id", sa.Integer(), nullable=False),
sa.Column("user_id", sa.Integer(), nullable=False),
sa.Column("hashed_code", sa.String(length=255), nullable=False),
sa.Column("is_used", sa.Boolean(), nullable=False),
sa.ForeignKeyConstraint(
["user_id"],
["users.id"],
),
sa.PrimaryKeyConstraint("id"),
)
op.create_index(
"ix_mfa_recovery_codes_hashed_code",
"mfa_recovery_codes",
["hashed_code"],
unique=True,
)
op.create_index(
op.f("ix_mfa_recovery_codes_id"), "mfa_recovery_codes", ["id"], unique=False
)
op.create_index(
"ix_mfa_recovery_codes_user_id", "mfa_recovery_codes", ["user_id"], unique=False
)
op.create_index('ix_mfa_recovery_codes_hashed_code', 'mfa_recovery_codes', ['hashed_code'], unique=True)
op.create_index(op.f('ix_mfa_recovery_codes_id'), 'mfa_recovery_codes', ['id'], unique=False)
op.create_index('ix_mfa_recovery_codes_user_id', 'mfa_recovery_codes', ['user_id'], unique=False)
# ### end Alembic commands ###


def downgrade() -> None:
"""Downgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.drop_index('ix_mfa_recovery_codes_user_id', table_name='mfa_recovery_codes')
op.drop_index(op.f('ix_mfa_recovery_codes_id'), table_name='mfa_recovery_codes')
op.drop_index('ix_mfa_recovery_codes_hashed_code', table_name='mfa_recovery_codes')
op.drop_table('mfa_recovery_codes')
op.drop_index("ix_mfa_recovery_codes_user_id", table_name="mfa_recovery_codes")
op.drop_index(op.f("ix_mfa_recovery_codes_id"), table_name="mfa_recovery_codes")
op.drop_index("ix_mfa_recovery_codes_hashed_code", table_name="mfa_recovery_codes")
op.drop_table("mfa_recovery_codes")
# ### end Alembic commands ###
Loading
Loading