Skip to content

Repository files navigation

Orion

API em ASP.NET Core (.NET 10) estruturada como modular monolith, com módulos de negócio isolados em projetos próprios, autenticação via JWT Bearer Token e bloqueio de conta por tentativas de login falhas.

Autor

Raimundos Marques — Analista de Sistemas

Projeto construído com propósito duplo: consolidar estudo de ASP.NET Core e arquitetura modular monolith, com a intenção de evoluir para um serviço em produção.

Sumário

Arquitetura

Modular monolith: um único processo/deploy, mas cada módulo de negócio é um projeto (.csproj) separado, não só uma pasta. Isso faz o isolamento ser garantido pelo compilador — um módulo literalmente não compila se referenciar outro — em vez de depender só de disciplina de code review.

Api (host)  ──references──▶  Modules/Users
            ──references──▶  Modules/Authentication
            ──references──▶  Orion.SharedKernel

Modules/Users          ──references──▶  Orion.SharedKernel
Modules/Authentication ──references──▶  Orion.SharedKernel

Modules/Users  ✗  Modules/Authentication   (nunca se referenciam entre si)

Dentro de cada módulo, a separação em camadas é feita por pasta, não por projeto:

Modules/Users/
├── Domain/            entidade rica (User), sem dependência de framework
│   └── Abstractions/  interfaces que o Domain define e a Infrastructure implementa
├── Application/       casos de uso (um por ação: CreateUserUseCase, RenameUserUseCase...)
│   └── Abstractions/  interfaces que a Application usa e a Infrastructure implementa
└── Infrastructure/    EF Core, hashing de senha, controllers HTTP
    ├── Persistence/
    ├── Security/
    └── Web/

Um projeto por módulo + camadas por pasta foi uma decisão deliberada: separar camadas em projetos também (Users.Domain.csproj, Users.Application.csproj, ...) multiplicaria a quantidade de .csproj sem ganho proporcional de rigor — a mesma técnica de teste de arquitetura usada pra isolar módulos serviria igualmente pra isolar camada, sem precisar do overhead de projeto por camada.

Comunicação entre módulos sem acoplamento direto

O módulo Authentication precisa validar credenciais que pertencem ao módulo Users, mas não pode referenciar Users diretamente — isso quebraria o isolamento. A solução é inversão de dependência via um contrato no Orion.SharedKernel:

  • Orion.SharedKernel.Contracts.IUserCredentialsChecker — contrato, visível a todos os módulos.
  • Users.Infrastructure.Security.UserCredentialsChecker — implementação real, dentro do módulo Users.
  • Authentication depende só da interface; a implementação concreta é resolvida em runtime pelo container de DI, configurado no host (Api).

Orion.SharedKernel

Projeto compartilhado com o mínimo indispensável usado por todos os módulos, sem dependência de ASP.NET Core:

  • Entity<TId> — base de entidade de domínio (igualdade por identidade).
  • AppException/ErrorType — hierarquia de exceções de aplicação (AppNotFoundException, AppConflictException, AppUnauthorizedException, AppLockedException, ...).
  • Contracts/IUserCredentialsChecker — contrato cross-module descrito acima.

Teste de isolamento de módulos

src/Tests/Architecture/ModuleIsolationTests.cs lê o XML de cada .csproj de módulo e falha se encontrar um <ProjectReference> de um módulo apontando pra outro. Roda como parte da suíte de testes normal — qualquer tentativa de acoplar Users e Authentication diretamente quebra o build de testes, não só uma revisão de código.

Stack técnica

Categoria Tecnologia
Runtime / framework .NET 10 / ASP.NET Core Web API
Persistência Entity Framework Core 10 + SQL Server
Autenticação JWT Bearer (Microsoft.AspNetCore.Authentication.JwtBearer)
Hash de senha Microsoft.AspNetCore.Identity.PasswordHasher<TUser>
Documentação de API OpenAPI nativo (Microsoft.AspNetCore.OpenApi) + Swagger UI (Swashbuckle.AspNetCore.SwaggerUI)
Testes xUnit + Moq
Health check Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore
Containerização Docker multi-stage build

Módulos

Users

Dono do ciclo de vida da conta: criação, renomear, trocar senha, verificar email, ativar/desativar. Schema próprio no banco (users).

Authentication

Dono da autenticação: login, emissão de JWT e bloqueio de conta por tentativas falhas. Schema próprio no banco (auth), completamente independente do schema users — a única coisa que os dois módulos compartilham é o contrato IUserCredentialsChecker.

Segurança

  • Hash de senha: PasswordHasher<User> do ASP.NET Core Identity (PBKDF2-HMACSHA256, 100k iterações, salt aleatório) — mantido oficialmente pela Microsoft, sem dependência de terceiro.
  • Autenticação: JWT Bearer assinado com HMAC-SHA256. Claims: sub (id do usuário), email, jti. MapInboundClaims = false garante que essas claims cheguem intactas no HttpContext.User, sem o remapeamento legado do .NET.
  • Autorização: endpoints de Users exigem [Authorize] e checam que o dono do token (sub) é o mesmo {id} da rota — outro usuário autenticado recebe 403, não 200. POST /api/users e todo /api/auth/* são públicos por natureza (registro e login/refresh não têm token ainda).
  • Refresh token: emitido junto do access token no login (Authentication.Domain.RefreshToken), com rotação — cada POST /api/auth/refresh revoga o token usado e emite um novo par. Reuso de um refresh token já rotacionado ou revogado é rejeitado (401).
  • Logout: POST /api/auth/logout revoga o refresh token informado. Não invalida o access token já emitido (ele expira sozinho pelo exp, curto por design).
  • Bloqueio de conta (Authentication.Domain.LoginLockoutPolicy): 5 tentativas de login falhas em uma janela de 15 minutos bloqueiam novas tentativas — inclusive com a senha correta — até 15 minutos após a última falha. Cada tentativa (sucesso ou falha) é registrada em auth.LoginAttempts.

Tratamento de erros

Toda exceção de domínio/aplicação herda de AppException e carrega um ErrorType. O GlobalExceptionHandler (Api/Exceptions) converte isso em ProblemDetails com o status HTTP correspondente:

ErrorType HTTP
Validation 400
Unauthorized 401
Forbidden 403
NotFound 404
Conflict 409
Locked 423
Unexpected (qualquer outra exceção) 500

Endpoints

Users — /api/users

Todos exigem Authorization: Bearer <token>, exceto a criação. Todos (exceto a criação) só aceitam operar sobre o {id} do próprio token — outro usuário autenticado recebe 403.

Método Rota Ação Auth
POST /api/users Cria usuário público
GET /api/users/{id} Busca usuário por id [Authorize] + dono
PUT /api/users/{id}/name Renomeia [Authorize] + dono
PUT /api/users/{id}/password Troca senha [Authorize] + dono
POST /api/users/{id}/verify-email Marca email como verificado [Authorize] + dono
POST /api/users/{id}/activate Ativa [Authorize] + dono
POST /api/users/{id}/deactivate Desativa [Authorize] + dono

Authentication — /api/auth

Todos públicos — são justamente os endpoints usados antes de (ou sem) ter um access token válido.

Método Rota Ação
POST /api/auth/login Login — retorna access token + refresh token
POST /api/auth/refresh Troca um refresh token válido por um novo par (rotação)
POST /api/auth/logout Revoga um refresh token

Infra

Método Rota Ação
GET /health Verifica conectividade com os dois bancos (Users + Authentication)

Documentação interativa em /swagger (ambiente Development).

Testes

  • src/Tests/Modules/Users — testes unitários do módulo Users (domínio + use cases com Moq).
  • src/Tests/Architecture — teste de isolamento entre módulos.
dotnet test Orion.slnx

Como rodar localmente

Pré-requisitos: .NET 10 SDK, SQL Server acessível, ferramenta dotnet-ef (dotnet tool install --global dotnet-ef).

# 1. Configurar segredos (nunca em appsettings.json)
dotnet user-secrets set "ConnectionStrings:DatabaseConnection" "<sua connection string>" --project src/Api/Api.csproj
dotnet user-secrets set "Jwt:SigningKey" "<chave aleatória de pelo menos 32 bytes>" --project src/Api/Api.csproj

# 2. Aplicar as migrations de cada módulo
dotnet ef database update --project src/Modules/Users/Users.csproj --startup-project src/Api/Api.csproj
dotnet ef database update --project src/Modules/Authentication/Authentication.csproj --startup-project src/Api/Api.csproj --context AuthenticationDbContext

# 3. Rodar
dotnet run --project src/Api/Api.csproj

Docker

Dois caminhos, propositalmente separados:

  • Devdocker compose up aplica docker-compose.yml + docker-compose.override.yml automaticamente. Monta o user-secrets e o certificado de desenvolvimento da máquina host; força ASPNETCORE_ENVIRONMENT=Development. Nunca use isso em produção.
  • Produção — precisa ser explicitamente pedido, não é aplicado por acidente:
    cp .env.production.example .env   # preencher com valores reais
    docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d
    Segredos entram via variável de ambiente (ConnectionStrings__DatabaseConnection, Jwt__SigningKey), não via arquivo. A imagem expõe HEALTHCHECK batendo em /health.

CI/CD

.github/workflows/deploy.yml roda a cada push em main (ou manualmente via workflow_dispatch), em três estágios sequenciais:

  1. testdotnet build + dotnet test na solução inteira, num runner hospedado pelo GitHub (ubuntu-latest).
  2. migrate — aplica as migrations pendentes de Users e Authentication contra o banco de produção (dotnet ef database update, um por módulo). Só roda se test passar.
  3. deploy — chama a API do Dokploy (POST /api/compose.deploy) pra disparar o redeploy do app. Só roda se migrate passar.

Por que migrate roda num runner self-hosted, não no ubuntu-latest

Um runner hospedado pelo GitHub roda na nuvem — ele não alcançaria um SQL Server on-premise sem exposição pra internet. Por isso o job migrate usa runs-on: self-hosted: você precisa registrar um runner self-hosted numa máquina da sua própria rede (que já enxergue o banco), em Settings → Actions → Runners → New self-hosted runner. É esse runner — não um do GitHub — que efetivamente executa o dotnet ef database update.

Deploy via Dokploy, não via SSH/Docker manual

A aplicação está conectada ao Dokploy por integração de GitHub App, com o app configurado como tipo Compose — o próprio Dokploy clona o repo e builda a partir do docker-compose.yml, sem precisarmos publicar imagem em nenhum registry.

Importante: o toggle Auto Deploy dessa integração deve ficar desligado no painel do Dokploy. Se ele ficar ligado, o Dokploy tentaria deployar direto a cada push (via webhook do próprio GitHub App), em paralelo com esse workflow — e a ordem test → migrate → deploy deixaria de ser garantida (o container novo podendo subir antes da migration terminar). Com o toggle desligado, quem decide quando deployar é exclusivamente o job deploy, chamando a API depois que tudo antes dele passou.

Secrets necessários no repositório GitHub (Settings → Secrets and variables → Actions):

Secret Usado em Onde conseguir
DATABASE_CONNECTION_STRING migrate a mesma connection string do .env de produção
JWT_SIGNING_KEY migrate (a checagem em Program.cs roda mesmo só pra gerar a migration) a mesma chave do .env de produção
DOKPLOY_URL deploy URL base da sua instância Dokploy, sem barra no final
DOKPLOY_API_TOKEN deploy perfil Dokploy → API/CLI → Generate API Key
DOKPLOY_COMPOSE_ID deploy aparece na URL da aplicação no painel do Dokploy

Estrutura de pastas

src/
├── Api/                          host ASP.NET Core — Program.cs, Dockerfile, appsettings
├── Modules/
│   ├── Users/                    Domain / Application / Infrastructure
│   └── Authentication/           Domain / Application / Infrastructure
├── Orion.SharedKernel/           Entity<TId>, AppException, contratos cross-module
└── Tests/
    ├── Architecture/             isolamento entre módulos
    └── Modules/Users/            testes unitários do módulo Users

About

API em ASP.NET Core (.NET 10) estruturada como modular monolith, com módulos de negócio isolados em projetos próprios

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages