CourseCore API e um backend modular em ASP.NET Core para uma plataforma de cursos. A API cobre usuarios, roles, permissions, areas, cursos, modulos, aulas, videos, progresso, auditoria e autenticacao JWT.
O projeto ja inclui hardening de configuracao para producao, refresh token persistido com hash e rotacao, permission claims, health checks, observabilidade com correlation id, audit logs de acoes sensiveis, Docker/Docker Compose, CI e testes automatizados.
GET /api/users usa paginacao com page=1, pageSize=50 e maximo 100. Cursos disponiveis resolvem acessos de area em lote, e grants repetidos atualizam a concessao existente.
- .NET 10
- ASP.NET Core
- Entity Framework Core
- PostgreSQL
- JWT Bearer
- BCrypt
- Scalar/OpenAPI
- Docker e Docker Compose
- GitHub Actions
- xUnit
- SQLite in-memory nos testes de integracao HTTP
O projeto segue Clean Architecture / DDD modular, organizado por modulos de negocio:
Modules/
Auth/
Users/
Access/
Courses/
Media/
Progress/
AuditLogs/
Shared/
As entidades de dominio ficam separadas dos PersistenceModels. O CourseCoreDbContext mapeia apenas modelos de persistencia. Controllers chamam use cases, use cases dependem de interfaces, e repositories EF convertem PersistenceModel <-> Domain por mappers.
Para detalhes, veja Docs/coursecore-implementation-plan-modular.md e Docs/implementation-class-diagram.md.
- Git
- .NET SDK 10
- PostgreSQL, para rodar localmente sem Docker
- Docker Desktop, para rodar com Docker Compose
- Ferramenta
dotnet-ef, para comandos de migrations
Comandos uteis:
dotnet --version
dotnet tool list
docker --version
docker compose versionSe o dotnet-ef nao estiver disponivel, instale ou restaure conforme a sua configuracao local:
dotnet tool install --global dotnet-efArquivos e fontes de configuracao:
appsettings.json: defaults seguros. Nao deve conter secrets reais.appsettings.Development.json: valores locais de desenvolvimento..env: arquivo local opcional, ignorado pelo Git. Nao e carregado automaticamente pela API..env.example: arquivo versionado com placeholders para copiar e adaptar fora do repositorio.- Variaveis de ambiente: fonte recomendada para secrets em homologacao/producao.
Principais variaveis da API:
ASPNETCORE_ENVIRONMENT
ConnectionStrings__CourseCoreDatabase
Jwt__SecretKey
Jwt__Issuer
Jwt__Audience
Jwt__AccessTokenExpirationMinutes
Jwt__RefreshTokenExpirationDays
Auth__ExposeRefreshTokenInBody
Auth__AllowRefreshTokenInBodyFallback
Auth__RefreshTokenCookie__Name
Auth__RefreshTokenCookie__Path
Auth__RefreshTokenCookie__SameSite
Auth__RefreshTokenCookie__Secure
Auth__RefreshTokenCookie__MaxAgeDays
RateLimiting__Login__PermitLimit
RateLimiting__Login__WindowSeconds
RateLimiting__Refresh__PermitLimit
RateLimiting__Refresh__WindowSeconds
RateLimiting__Logout__PermitLimit
RateLimiting__Logout__WindowSeconds
RateLimiting__Register__PermitLimit
RateLimiting__Register__WindowSeconds
RateLimiting__ResendConfirmation__PermitLimit
RateLimiting__ResendConfirmation__WindowSeconds
Turnstile__SecretKey
Resend__ApiKey
Resend__FromAddress
Resend__FromName
Progress__LessonCompletionThresholdPercent
Media__Playback__SignedUrlExpirationMinutes
Media__Playback__SigningSecret
Media__Playback__BaseUrl
Media__Playback__AllowedStorageProviders__0
Cors__AllowedOrigins__0
Seed__Admin__Enabled
Seed__Admin__Name
Seed__Admin__Email
Seed__Admin__Password
Seed__Admin__ResetPassword
Variaveis usadas pelo Docker Compose:
POSTGRES_DB
POSTGRES_USER
POSTGRES_PASSWORD
POSTGRES_PORT
COURSECORE_API_HTTP_PORT
Em producao, configure secrets e connection strings por variaveis protegidas ou secret manager. Nao coloque senha, JWT secret, token ou connection string real em arquivos versionados.
- Restaure e compile:
dotnet restore
dotnet build-
Garanta que o PostgreSQL local esta rodando.
-
Configure a connection string local por variavel de ambiente ou pelo
appsettings.Development.json.
Exemplo via PowerShell, usando placeholders:
$env:ASPNETCORE_ENVIRONMENT="Development"
$env:ConnectionStrings__CourseCoreDatabase="Host=127.0.0.1;Port=5432;Database=coursecore;Username=SEU_USUARIO;Password=SUA_SENHA;Timeout=15;Command Timeout=60"- Aplique migrations manualmente:
dotnet ef database update --context CourseCoreDbContext- Rode a API:
dotnet runEm Development, a API expõe Scalar/OpenAPI. A porta exata depende do profile local em Properties/launchSettings.json ou da variavel ASPNETCORE_URLS.
Valide a configuracao do compose:
docker compose configSuba API e PostgreSQL:
docker compose up --buildLogs:
docker compose logs -f coursecore-api
docker compose logs -f coursecore-postgresParar containers:
docker compose downAviso: docker compose down -v remove o volume local do PostgreSQL e apaga os dados locais do banco.
O Docker Compose executa o servico one-shot coursecore-migrations antes da API. O EF Core consulta __EFMigrationsHistory, ignora migrations ja aplicadas e executa somente as pendentes; se houver falha, a API nao inicia. O seed continua desabilitado por padrao (Seed__Admin__Enabled=false). Para staging/producao, prefira scripts SQL revisados conforme Docs/deployment-migrations.md.
O compose exige ambiente, senha do PostgreSQL, JWT secret, media signing secret e senha de seed explicitamente no .env. O override local publica o PostgreSQL; use somente o arquivo base em producao para manter o banco privado. A imagem da API roda com o usuario nao-root nativo do runtime .NET. Veja Docs/docker.md.
Migrations nao rodam automaticamente no startup da API. Aplique localmente apenas quando apropriado:
dotnet ef migrations list --context CourseCoreDbContext
dotnet ef database update --context CourseCoreDbContextPara homologacao e producao, gere um SQL idempotente e revise antes de aplicar:
dotnet ef migrations script --context CourseCoreDbContext --idempotent --output ./artifacts/migrations/coursecore-migration.sqlOu use os scripts do projeto:
./scripts/generate-migration-script.ps1./scripts/generate-migration-script.shO SQL gerado fica em artifacts/migrations/, que e ignorado pelo Git. Veja Docs/deployment-migrations.md.
O seed admin e idempotente, opt-in e roda somente em Development. Ele nao aplica migrations; o schema precisa estar atualizado antes.
Exemplo seguro com placeholders:
$env:Seed__Admin__Enabled="true"
$env:Seed__Admin__Name="CourseCore Admin"
$env:Seed__Admin__Email="admin@coursecore.local"
$env:Seed__Admin__Password="CHANGE_ME_LOCAL_ONLY"
$env:Seed__Admin__ResetPassword="false"
dotnet runPara redefinir a senha local de forma intencional:
$env:Seed__Admin__ResetPassword="true"Veja Docs/database-seeding.md.
dotnet test
dotnet test --configuration ReleaseOs testes de integracao HTTP usam SQLite in-memory via WebApplicationFactory. Eles nao dependem de PostgreSQL real, nao executam migrations e nao rodam seed real.
Os testes de seguranca exercitam o pipeline HTTP real para rotacao concorrente e replay de refresh token, logout, revogacao por TokenVersion, rate limiting, policies e validacao de requests. Limites menores de rate limiting sao aplicados apenas por configuracao isolada da factory de teste. Nao sao necessarios Docker, PostgreSQL ou servicos externos. A revogacao dinamica de roles nao e exercitada porque a API ainda nao possui operacao publica de remocao/desativacao de role atribuida.
O projeto inclui uma collection Postman completa para os 31 endpoints executaveis, com Bearer automatico, refresh por cookie HttpOnly, encadeamento de IDs, cenarios negativos e um environment local somente com placeholders:
Postman/CourseCore.postman_collection.json
Postman/CourseCore.local.postman_environment.json
Use Docs/postman.md para configurar baseUrl e credenciais locais, executar o fluxo numerado no Collection Runner, renovar o access token por cookie e entender quais IDs sao automaticos ou dependem de dados existentes. A API nao expoe atualmente endpoint me nem listagem HTTP de audit logs.
Endpoints:
GET /health/live
GET /health/ready
GET /health
/health/live: valida o processo da API e nao depende do banco./health/ready: valida conectividade/preparo do banco./health: agrega os checks configurados.
/health/live sempre retorna somente o status agregado. Em Development, ready e health incluem detalhes uteis; em producao retornam somente o status e devem permanecer internos, protegidos pela rede ou reverse proxy.
Em Development:
GET /openapi/v1.json
GET /scalar
O Bearer JWT aparece por endpoint protegido. Endpoints publicos como login e refresh token permanecem sem requisito Bearer na documentacao.
Scalar/OpenAPI nao sao expostos por padrao em Production.
- Login emite JWT.
- Registro publico (
POST /api/auth/register) cria conta sem nenhuma role, protegido por CAPTCHA (Cloudflare Turnstile) e ja autentica no mesmo request. - Conta recem-registrada nasce com e-mail nao confirmado; nenhum acesso a conteudo de curso (nem curso gratuito) e liberado ate
POST /api/auth/confirm-emailser chamado com o token enviado por e-mail (Resend). O catalogo (GET /api/courses/available) continua visivel antes da confirmacao, so que com tudo bloqueado. - Login, registro, refresh token, logout e reenvio de confirmacao possuem rate limiting e retornam
429 Too Many Requestsquando o limite configurado e excedido. - Login de e-mail inexistente executa uma verificacao BCrypt ficticia para reduzir enumeracao por diferenca de tempo.
- Refresh token e persistido somente como hash.
- Refresh token e enviado para clientes web em cookie
HttpOnly. - Refresh token possui expiracao, revogacao e rotacao atomica.
- Reutilizacao de refresh token antigo e rejeitada.
- Logout revoga o refresh token da sessao atual.
- Access token continua no body da resposta; refresh token nao e exposto no body em Production.
- JWT inclui roles, permission claims e
token_version. - Requests autenticadas validam o usuario atual no banco e rejeitam JWT antigo quando
TokenVersiondiverge ou o usuario esta inativo. - Atualizacoes criticas de usuario incrementam
TokenVersione revogam refresh tokens ativos do usuario afetado. - Policies usam permissions com fallback para a role
Admin. - Fluxos sensiveis usam o usuario autenticado pelo token, nao
userIdenviado pelo cliente.
Policies de acesso sao separadas por operacao:
ManageCourseAccess -> users.manage ou Admin
ManageAreas -> areas.manage ou Admin
CheckOwnCourseAccess -> qualquer usuario autenticado
CheckUserCourseAccess -> users.manage, areas.manage, courses.manage ou Admin
GET /api/access/courses/{courseId} consulta somente o acesso do usuario autenticado. GET /api/access/users/{userId}/courses/{courseId} e a consulta administrativa explicita de um usuario alvo. O antigo POST /api/access/course/check permanece deprecated para compatibilidade e continua limitado ao usuario do token. Grants para roles inativas sao rejeitados com conflito conhecido.
CRUD administrativo de areas (POST/PUT/GET /api/areas, GET /api/areas/{areaId}) fica em um controller separado de /api/access, protegido por ManageAreas. Nao ha remocao fisica: active=false via PUT e a forma suportada de retirar uma area de uso, e derruba imediatamente o acesso a cursos vinculados a ela.
A API usa o header:
X-Correlation-ID
Se o cliente envia um GUID valido, a API preserva o valor. Caso contrario, a API gera um novo correlation id. O response devolve X-Correlation-ID, e respostas de erro incluem traceId e correlationId.
Logs de aplicacao nao devem conter senha, access token, refresh token, hash, JWT secret, connection string ou secrets de ambiente.
Criacao de usuario e seed administrativo usam uma politica central de senha: minimo de 12 caracteres e maximo de 72 bytes UTF-8, alinhado ao limite seguro do BCrypt, sem valores vazios ou senhas comuns basicas. A senha nunca e retornada, auditada ou registrada em logs.
Campos administrativos sao validados antes da persistencia conforme os limites do banco. Create Course aceita ate 50 areas, 50 modulos e 100 aulas por modulo; thumbnails devem ser URLs HTTP(S) absolutas. Videos limitam titulo, descricao, storage key, duracao e tamanho declarado. O corpo HTTP no Kestrel e limitado a 1 MiB.
Erros de validacao conhecidos retornam 400. Excecoes operacionais inesperadas retornam 500; fora de Development, mensagens internas nao sao expostas. Respostas de erro preservam traceId e correlationId.
Cookies de refresh token usam HttpOnly, path /api/auth e SameSite=Lax por padrao. Em Production, o cookie e sempre Secure. Esta etapa nao habilita CORS com credentials nem protecao CSRF completa; para deployments cross-site, avalie CORS explicito com credentials, SameSite=None; Secure e CSRF token/custom header em etapa propria.
Rate limiting reduz brute force, credential stuffing e abuso operacional, mas nao substitui MFA, CAPTCHA, lockout progressivo ou monitoramento antifraude em uma etapa futura.
Validacao de token_version consulta o banco em requests autenticadas. Otimizacoes futuras podem usar cache curto por userId/tokenVersion, cache distribuido ou validacao mais seletiva em endpoints sensiveis.
Veja Docs/observability.md.
O cliente registra apenas watchedSeconds. A API calcula a conclusao da aula no servidor usando a duracao real do video e o threshold configurado em Progress:LessonCompletionThresholdPercent, com default de 90%.
WatchedSeconds e monotonico: uma chamada posterior com valor menor nao reduz o progresso ja salvo. Quando existe video com duracao conhecida, o valor salvo tambem e limitado a Video.DurationSeconds.
O campo markAsCompleted ainda e aceito temporariamente no request para compatibilidade, mas esta deprecated e e ignorado pelo servidor. A aula so e concluida quando o video esta Ready, possui DurationSeconds > 0 e o progresso assistido atinge o percentual minimo configurado. Curso concluido e percentual do curso sao recalculados somente a partir de aulas concluidas por essa regra do servidor.
playbackUrl em CreateVideo continua aceito temporariamente por compatibilidade, mas esta deprecated e e ignorado. O backend nao usa URL arbitraria enviada pelo cliente para marcar video como pronto nem para responder playback.
Videos novos iniciam como Processing. Um administrador com ManageVideos deve marcar o video como pronto por POST /api/videos/{id}/ready, sem enviar URL. O endpoint de playback gera uma URL temporaria assinada no momento da requisicao, com expiresAt, usando Media:Playback:SigningSecret, BaseUrl, SignedUrlExpirationMinutes e AllowedStorageProviders.
StorageKey e uma chave interna de storage, nao uma URL publica. A API rejeita storage keys vazias, URLs completas, path traversal e caracteres inseguros. Integracao com storage/proxy real fica para etapa futura; a URL assinada atual e um contrato seguro para essa integracao.
Eventos sensiveis auditados:
LoginSucceededRefreshTokenRotatedRefreshTokenRejectedRefreshTokenReplayDetectedLogoutSucceededUserCreatedUserUpdatedUserAreaAccessGrantedRoleAreaAccessGrantedCourseCreatedCourseUpdatedCoursePublishedVideoCreated
Audit logs registram metadados seguros e correlation id quando disponivel. Nao sao auditados senha, JWT, refresh token, hash do refresh token, storage key, playback URL ou secrets.
O workflow fica em .github/workflows/ci.yml e roda em push e pull_request para master e main.
Ele executa:
dotnet restore
dotnet build --no-restore --configuration Release
dotnet test --no-build --configuration Release --verbosity normal
dotnet list package --vulnerable --include-transitive
O CI nao usa PostgreSQL real, nao aplica migrations e nao roda seed real. Veja Docs/ci.md.
CourseCore/
Modules/
Auth/
Users/
Access/
Courses/
Media/
Progress/
AuditLogs/
Shared/
Docs/
Tests/
scripts/
Docs/coursecore-implementation-plan-modular.mdDocs/coursecore-extra-implementation-plan.mdDocs/coursecore-post-readiness-extra-plan.mdDocs/implementation-class-diagram.mdDocs/production-configuration.mdDocs/docker.mdDocs/database-seeding.mdDocs/deployment-migrations.mdDocs/observability.mdDocs/ci.mdDocs/postman.mdDocs/specs/— specs e implementation plans por feature (WHAT/WHY na spec, HOW no plan)
- Configurar secrets via variaveis protegidas ou secret manager.
- Configurar
Turnstile__SecretKeyeResend__ApiKey/Resend__FromAddress— obrigatorios emProduction(registro publico e confirmacao de e-mail nao sobem sem eles). - Configurar connection string real fora do repositorio.
- Configurar CORS restrito.
- Configurar HTTPS/HSTS e reverse proxy conforme a infraestrutura.
- Avaliar
ForwardedHeaderscom proxies confiaveis. - Aplicar migrations fora do startup, com SQL revisado.
- Manter seed desabilitado.
- Nao expor Scalar/OpenAPI em
Production. - Validar
/health/live,/health/readye/healthapos deploy. - Configurar logs, metricas e monitoramento externo conforme a operacao evoluir.