diff --git a/README.md b/README.md
index ef618ae..0af855d 100644
--- a/README.md
+++ b/README.md
@@ -1,78 +1,306 @@
-# 🐴 GoHorse Jobs (Vagas para Tecnologia)
+# 🐴 GoHorse Jobs
-A comprehensive recruitment platform connecting job seekers with opportunities in the technology sector.
+[](https://golang.org/)
+[](https://nextjs.org/)
+[](https://postgresql.org/)
+[](https://docker.com/)
+[](LICENSE)
-## 📋 Project Overview
+> 🇧🇷 Plataforma de recrutamento conectando profissionais de tecnologia a oportunidades de emprego.
-GoHorse Jobs is a multi-service application designed to facilitate the hiring process. It consists of a high-performance Go backend, a modern Next.js frontend, and a specialized Seeder API for generating realistic test data.
+---
-## 🔐 Credentials (Test Environment)
+## 📋 Índice
-Use these credentials to access the different dashboards:
+- [Visão Geral](#-visão-geral)
+- [Arquitetura](#-arquitetura)
+- [Tech Stack](#-tech-stack)
+- [Pré-requisitos](#-pré-requisitos)
+- [Instalação](#-instalação)
+- [Variáveis de Ambiente](#-variáveis-de-ambiente)
+- [Scripts Disponíveis](#-scripts-disponíveis)
+- [Credenciais de Teste](#-credenciais-de-teste)
+- [Documentação da API](#-documentação-da-api)
+- [Estrutura de Pastas](#-estrutura-de-pastas)
-| User Type | Identifier | Password | Dashboard |
-|-----------|------------|----------|-----------|
-| **SuperAdmin** | `superadmin` | `Admin@2025!` | `/dashboard/admin` (System Stats) |
-| **Company Admin** | `takeshi_yamamoto` | `Takeshi@2025` | `/dashboard/empresa` (Manage Jobs) |
-| **Recruiter** | `maria_santos` | `User@2025` | `/dashboard/empresa` (View Candidates) |
-| **Candidate** | `paulo_santos` | `User@2025` | `/dashboard/candidato` (Apply for Jobs) |
+---
-## 🏗️ Architecture
+## 🎯 Visão Geral
-The project follows a microservices-inspired architecture:
+**GoHorse Jobs** é uma plataforma completa de recrutamento que permite:
+
+- 🏢 **Empresas**: Publicar vagas, gerenciar candidaturas e comunicar-se com candidatos
+- 👤 **Candidatos**: Buscar vagas, candidatar-se e acompanhar status
+- 👑 **Administradores**: Gerenciar todo o sistema com painel administrativo
+
+---
+
+## 🏗️ Arquitetura
+
+O projeto segue uma arquitetura de **microserviços** com três componentes principais:
```mermaid
-graph TD
- User((User))
- Frontend[Frontend (Next.js)]
- Backend[Backend (Go/Gin)]
- Seeder[Seeder API (Node.js)]
- DB[(PostgreSQL)]
-
- User --> Frontend
- Frontend -->|HTTP/REST| Backend
- Seeder -->|Writes| DB
- Backend -->|Reads/Writes| DB
+graph TB
+ subgraph Frontend
+ A[Next.js 15
App Router]
+ end
+
+ subgraph Backend
+ B[Go API
Clean Architecture]
+ end
+
+ subgraph Database
+ C[(PostgreSQL 16)]
+ end
+
+ subgraph Seeder
+ D[Node.js
Seeder API]
+ end
+
+ A -->|REST API| B
+ B -->|GORM| C
+ D -->|pg client| C
```
-## 🚀 Quick Start
+### Padrões Arquiteturais
-### 1. Run the Backend API
-We have provided a convenience script to start the backend quickly.
+| Componente | Padrão | Descrição |
+|------------|--------|-----------|
+| **Backend** | Clean Architecture | Separação em `handlers`, `services`, `models`, `usecases` |
+| **Frontend** | App Router | Next.js 15 com Server Components e Client Components |
+| **Multi-tenancy** | JWT + Company Context | Isolamento de dados por empresa via middleware |
+
+---
+
+## 🛠️ Tech Stack
+
+### Backend
+| Tecnologia | Versão | Uso |
+|------------|--------|-----|
+| Go | 1.24 | Linguagem principal |
+| PostgreSQL | 16 | Banco de dados |
+| JWT | v5 | Autenticação |
+| Swagger | 2.0 | Documentação API |
+
+### Frontend
+| Tecnologia | Versão | Uso |
+|------------|--------|-----|
+| Next.js | 15 | Framework React |
+| Tailwind CSS | 4 | Estilização |
+| shadcn/ui | - | Componentes UI |
+| Zod | 3.x | Validação de schemas |
+| React Hook Form | 7.x | Gerenciamento de forms |
+
+### DevOps
+| Tecnologia | Uso |
+|------------|-----|
+| Docker | Containerização |
+| Alpine Linux | Imagem base (mínima) |
+
+---
+
+## 📦 Pré-requisitos
+
+Antes de começar, certifique-se de ter instalado:
+
+- **Docker** (v24+) e **Docker Compose** (v2+)
+- **Go** (v1.24+) - para desenvolvimento local
+- **Node.js** (v20+) e **npm** - para frontend e seeder
+- **PostgreSQL** (v16+) - se rodar sem Docker
+
+---
+
+## 🚀 Instalação
+
+### Opção 1: Script de Desenvolvimento (Recomendado)
```bash
+# Clone o repositório
+git clone https://github.com/rede5/gohorsejobs.git
+cd gohorsejobs
+
+# Execute o script de desenvolvimento
./run_dev.sh
```
-### 2. Run the Frontend
+### Opção 2: Manual (Passo a Passo)
+
```bash
+# 1. Clone o repositório
+git clone https://github.com/rede5/gohorsejobs.git
+cd gohorsejobs
+
+# 2. Configure as variáveis de ambiente
+cp backend/.env.example backend/.env
+cp frontend/.env.example frontend/.env
+cp seeder-api/.env.example seeder-api/.env
+
+# 3. Inicie o banco de dados (PostgreSQL)
+# Certifique-se de ter um PostgreSQL rodando na porta 5432
+
+# 4. Inicie o Backend
+cd backend
+go run ./cmd/api
+
+# 5. Em outro terminal, inicie o Frontend
cd frontend
npm install
npm run dev
+
+# 6. (Opcional) Popule o banco com dados de teste
+cd seeder-api
+npm install
+npm run seed
```
-### 3. Database Setup & Seeding
-Refer to `seeder-api/README.md` for detailed instructions on populating the database.
+---
-## 📊 Status & Tasks
+## 🔐 Variáveis de Ambiente
-### Completed ✅
-- [x] Backend API Structure
-- [x] Docker Configuration
-- [x] Frontend Dashboard (Company)
-- [x] Seeder Logic (Users, Companies, Jobs)
-- [x] Documentation Unification
-- [x] Branding Update (GoHorse Jobs)
+### Backend (`backend/.env`)
-### In Progress 🚧
-- [ ] Integration of complete Frontend-Backend flow
-- [ ] Advanced Search Filters
-- [ ] Real-time Notifications
+| Variável | Descrição | Exemplo |
+|----------|-----------|---------|
+| `DB_HOST` | Host do PostgreSQL | `localhost` |
+| `DB_PORT` | Porta do PostgreSQL | `5432` |
+| `DB_USER` | Usuário do banco | `postgres` |
+| `DB_PASSWORD` | Senha do banco | `yourpassword` |
+| `DB_NAME` | Nome do banco | `gohorsejobs` |
+| `JWT_SECRET` | Secret para JWT (min 32 chars) | `your-secret-key-at-least-32-chars` |
+| `PORT` | Porta da API | `8080` |
+| `ENV` | Ambiente | `development` ou `production` |
+| `CORS_ORIGINS` | Origens permitidas | `http://localhost:3000` |
-## 📚 Documentation
-- [Backend Documentation](./backend/README.md)
-- [Frontend Documentation](./frontend/README.md)
-- [Seeder Documentation](./seeder-api/README.md)
+### Frontend (`frontend/.env`)
+
+| Variável | Descrição | Exemplo |
+|----------|-----------|---------|
+| `NEXT_PUBLIC_API_URL` | URL da API Backend | `http://localhost:8080` |
---
-*Generated by Antigravity*
+
+## 📜 Scripts Disponíveis
+
+### Raiz do Projeto
+
+| Comando | Descrição |
+|---------|-----------|
+| `./run_dev.sh` | Inicia backend + frontend em modo desenvolvimento |
+| `./run_dev_seed.sh` | Inicia tudo + popula banco com dados de teste |
+
+### Backend (`cd backend`)
+
+| Comando | Descrição |
+|---------|-----------|
+| `go run ./cmd/api` | Inicia o servidor de desenvolvimento |
+| `go build ./cmd/api` | Compila o binário |
+| `go test ./...` | Executa todos os testes |
+| `swag init -g cmd/api/main.go` | Regenera documentação Swagger |
+
+### Frontend (`cd frontend`)
+
+| Comando | Descrição |
+|---------|-----------|
+| `npm run dev` | Inicia servidor de desenvolvimento |
+| `npm run build` | Compila para produção |
+| `npm run start` | Inicia servidor de produção |
+| `npm run lint` | Executa linter |
+
+### Seeder API (`cd seeder-api`)
+
+| Comando | Descrição |
+|---------|-----------|
+| `npm run seed` | Popula banco com todos os dados |
+| `npm run seed:reset` | Limpa banco e repopula |
+| `npm run seed:users` | Popula apenas usuários |
+
+---
+
+## 🔑 Credenciais de Teste
+
+| Tipo | Usuário | Senha | Dashboard |
+|------|---------|-------|-----------|
+| **SuperAdmin** | `superadmin` | `Admin@2025!` | `/dashboard/admin` |
+| **Admin Empresa** | `takeshi_yamamoto` | `Takeshi@2025` | `/dashboard/empresa` |
+| **Recrutador** | `maria_santos` | `Maria@2025` | `/dashboard/empresa` |
+| **Candidato** | `paulo_santos` | `User@2025` | `/dashboard/candidato` |
+
+---
+
+## 📖 Documentação da API
+
+### Swagger UI
+Acesse a documentação interativa em: **http://localhost:8080/swagger/index.html**
+
+### Endpoints Principais
+
+| Método | Endpoint | Descrição | Autenticação |
+|--------|----------|-----------|--------------|
+| `GET` | `/health` | Health check | ❌ |
+| `POST` | `/api/v1/auth/login` | Login | ❌ |
+| `POST` | `/api/v1/companies` | Criar empresa | ❌ |
+| `GET` | `/api/v1/users` | Listar usuários | ✅ JWT |
+| `POST` | `/api/v1/users` | Criar usuário | ✅ JWT |
+| `GET` | `/jobs` | Listar vagas | ❌ |
+| `POST` | `/jobs` | Criar vaga | ✅ JWT |
+
+---
+
+## 📂 Estrutura de Pastas
+
+```
+gohorsejobs/
+├── backend/ # API Go
+│ ├── cmd/api/ # Entrypoint da aplicação
+│ ├── internal/ # Código interno
+│ │ ├── api/ # Handlers e middlewares (Clean Arch)
+│ │ ├── core/ # Domain e UseCases (DDD)
+│ │ ├── database/ # Conexão com banco
+│ │ ├── dto/ # Data Transfer Objects
+│ │ ├── handlers/ # Controllers (legacy)
+│ │ ├── middleware/ # Middlewares de segurança
+│ │ ├── models/ # Entidades do banco
+│ │ ├── router/ # Configuração de rotas
+│ │ ├── services/ # Lógica de negócios
+│ │ └── utils/ # Utilitários (JWT, sanitizer)
+│ ├── migrations/ # Migrations SQL
+│ └── docs/ # Swagger gerado
+│
+├── frontend/ # Next.js App
+│ └── src/
+│ ├── app/ # App Router (páginas)
+│ ├── components/ # Componentes React
+│ ├── contexts/ # React Contexts
+│ ├── hooks/ # Custom Hooks
+│ └── lib/ # Utilitários
+│
+├── seeder-api/ # Seeder Node.js
+│ └── src/
+│ ├── seeders/ # Scripts de seed por entidade
+│ └── index.js # Entrypoint
+│
+├── run_dev.sh # Script de desenvolvimento
+└── README.md # Este arquivo
+```
+
+---
+
+## 🤝 Contribuindo
+
+1. Fork o projeto
+2. Crie sua branch (`git checkout -b feature/AmazingFeature`)
+3. Commit suas mudanças (`git commit -m 'feat: add amazing feature'`)
+4. Push para a branch (`git push origin feature/AmazingFeature`)
+5. Abra um Pull Request
+
+---
+
+## 📄 Licença
+
+Este projeto está sob a licença MIT. Veja o arquivo [LICENSE](LICENSE) para mais detalhes.
+
+---
+
+
+ Desenvolvido com ❤️ pela equipe GoHorse +
diff --git a/backend/.dockerignore b/backend/.dockerignore new file mode 100644 index 0000000..ea7fdc0 --- /dev/null +++ b/backend/.dockerignore @@ -0,0 +1,47 @@ +# Dependencies +node_modules/ +.pnp +.pnp.js + +# Build outputs +/main +*.exe +*.dll +*.so +*.dylib + +# Test files +*_test.go +*.test +coverage.out +coverage.html + +# IDE and editor +.idea/ +.vscode/ +*.swp +*.swo +*~ + +# Git +.git/ +.gitignore + +# Documentation (keep docs/ for swagger) +*.md +LICENSE + +# Environment files (security) +.env +.env.* +!.env.example + +# Temporary files +tmp/ +temp/ +*.tmp +*.log + +# OS files +.DS_Store +Thumbs.db diff --git a/backend/Dockerfile b/backend/Dockerfile index 8f50d4a..ed72b32 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -1,37 +1,67 @@ -# Build stage +# ============================================================================= +# GoHorse Jobs Backend - Optimized Production Dockerfile +# ============================================================================= + +# ----------------------------------------------------------------------------- +# Stage 1: Build +# ----------------------------------------------------------------------------- FROM golang:1.24-alpine AS builder -WORKDIR /app +WORKDIR /build -# Install build dependencies if needed (e.g. gcc for cgo, though we disable cgo) -RUN apk add --no-cache git +# Install minimal build dependencies +RUN apk add --no-cache git ca-certificates tzdata +# Cache dependencies COPY go.mod go.sum ./ -RUN go mod download +RUN go mod download && go mod verify +# Copy source code COPY . . -# Build the application -RUN CGO_ENABLED=0 GOOS=linux go build -o main ./cmd/api +# Build with optimizations: +# - CGO_ENABLED=0: Static binary (no C dependencies) +# - ldflags -s -w: Strip debug info for smaller binary +# - trimpath: Remove local paths from binary +RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build \ + -ldflags="-s -w -X main.Version=$(git describe --tags --always --dirty 2>/dev/null || echo 'dev')" \ + -trimpath \ + -o /app/main ./cmd/api -# Run stage -FROM alpine:latest +# ----------------------------------------------------------------------------- +# Stage 2: Production (Minimal Image) +# ----------------------------------------------------------------------------- +FROM alpine:3.19 + +# Security: Run as non-root user +RUN addgroup -g 1001 -S appgroup && \ + adduser -u 1001 -S appuser -G appgroup WORKDIR /app -# Install runtime dependencies -RUN apk add --no-cache ca-certificates +# Copy timezone data and CA certificates from builder +COPY --from=builder /usr/share/zoneinfo /usr/share/zoneinfo +COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ -# Copy binary from builder +# Copy binary and migrations COPY --from=builder /app/main . +COPY --from=builder /build/migrations ./migrations -# Copy migrations (CRITICAL for auto-migration logic) -COPY --from=builder /app/migrations ./migrations +# Set ownership to non-root user +RUN chown -R appuser:appgroup /app + +# Switch to non-root user +USER appuser # Expose port EXPOSE 8080 -# Environment variables should be passed at runtime, but we can set defaults -ENV PORT=8080 +# Health check +HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ + CMD wget -qO- http://localhost:8080/health || exit 1 + +# Environment defaults +ENV PORT=8080 \ + TZ=America/Sao_Paulo CMD ["./main"] diff --git a/backend/README.md b/backend/README.md index 843ca38..7003850 100755 --- a/backend/README.md +++ b/backend/README.md @@ -1,196 +1,128 @@ -# GoHorseJobs Backend +# Backend - GoHorse Jobs API -This is the backend for the GoHorseJobs application, built with Go and PostgreSQL. +[](https://golang.org/) +[](https://postgresql.org/) -## Architecture - -### 1. **Core Module (Clean Architecture)** -The project now includes a strictly decoupled `internal/core` module that follows Clean Architecture and DDD principles. -- **Pure Domain**: `internal/core/domain/entity` (No external deps). -- **Ports**: `internal/core/ports` (Interfaces for Repositories/Services). -- **UseCases**: `internal/core/usecases` (Business Logic). - -### 2. **Multi-Tenancy** -- **Strict Isolation**: All core tables (`core_companies`, `core_users`) use UUIDs and strict Tenant FKs. -- **Middleware**: `TenantGuard` automatically extracts Tenant context from JWTs. - -### 3. **Swagger API Docs** -- **URL**: [http://localhost:8080/swagger/index.html](http://localhost:8080/swagger/index.html) -- **Generate**: `swag init -g cmd/api/main.go --parseDependency --parseInternal` - -### 4. **Super Admin Access** -A seed migration is provided to create the initial system access. -- **Migration**: `migrations/010_seed_super_admin.sql` -- **User**: `admin@gohorse.com` -- **Password**: `password123` - -## Prerequisites - -- Docker -- Docker Compose -- Go 1.22+ - -## Getting Started - -1. **Start the services:** - - ```bash - docker-compose up --build - ``` - - This will start the PostgreSQL database and the Go API server. - - Alternatively, use the running script in the root: - ```bash - ./run_dev.sh - ``` - -2. **Access the API:** - - The API will be available at `http://localhost:8080`. - - - **Health Check:** `GET /health` - - **List Jobs:** `GET /jobs` - - **Create Job:** `POST /jobs` - -## API Endpoints - -### `GET /health` - -Returns `200 OK` if the server is running. - -### `GET /jobs` - -Returns a list of all jobs. - -**Response:** - -```json -[ - { - "id": 1, - "title": "Software Engineer", - "description": "Develop Go applications.", - "company_name": "Tech Corp", - "location": "Remote", - "salary_range": "$100k - $120k", - "created_at": "2023-10-27T10:00:00Z" - } -] -``` - -### `POST /jobs` - -Creates a new job. - -**Request Body:** - -```json -{ - "title": "Software Engineer", - "description": "Develop Go applications.", - "company_name": "Tech Corp", - "location": "Remote", - "salary_range": "$100k - $120k" -} -``` - -**Response:** - -Returns the created job with its ID and creation timestamp. +API REST desenvolvida em Go seguindo princípios de **Clean Architecture** e **DDD**. --- -# 📚 Feature Documentation (Archives) +## 🏗️ Arquitetura -> [!NOTE] -> The following documentation describes features implemented in the application, including Frontend components and Local Database logic. They have been consolidated here for reference. +``` +internal/ +├── api/ # Clean Architecture Layer +│ ├── handlers/ # HTTP Handlers (Controllers) +│ └── middleware/ # Auth, CORS, Rate Limiting +│ +├── core/ # Domain Layer (DDD) +│ ├── domain/entity/ # Entidades puras (sem deps externas) +│ ├── ports/ # Interfaces (Repository, Service) +│ └── usecases/ # Casos de uso (Business Logic) +│ +├── infrastructure/ # Infrastructure Layer +│ ├── auth/ # JWT Service implementation +│ └── persistence/ # Repository implementations +│ +├── handlers/ # Legacy controllers +├── middleware/ # Security middlewares +├── models/ # GORM models +├── services/ # Business logic (legacy) +└── utils/ # Helpers (JWT, Sanitizer) +``` -## 🗄️ Local Database - Profile System (Legacy/Frontend) +--- -Full local database system using localStorage to manage profile photos and user data. +## 🔒 Segurança -### 🚀 Usage +### Middlewares Implementados -#### 1. Page with Database -Access: `http://localhost:3000/profile-db` +| Middleware | Arquivo | Descrição | +|------------|---------|-----------| +| **Auth** | `middleware/auth.go` | Validação JWT + RBAC | +| **CORS** | `middleware/cors.go` | Whitelist de origens via `CORS_ORIGINS` | +| **Rate Limiting** | `middleware/rate_limit.go` | 100 req/min por IP | +| **Security Headers** | `middleware/security_headers.go` | OWASP headers (XSS, CSP, etc.) | -#### 2. Features Implemented +### Validação de JWT -**📸 Profile Picture Upload** -- ✅ Click to select image -- ✅ Automatic validation (JPG, PNG, GIF, WebP) -- ✅ 2MB limit per file -- ✅ Instant preview -- ✅ Auto-save to localStorage -- ✅ Loading indicator -- ✅ Remove photo button +O servidor valida no boot que `JWT_SECRET` tenha pelo menos 32 caracteres. Em produção (`ENV=production`), o servidor **não inicia** com secrets fracos. -**🗃️ Local Database** -- ✅ Auto-save to localStorage -- ✅ Persistence between sessions -- ✅ Export data (JSON backup) -- ✅ Clear all data -- ✅ User structure -- ✅ Creation/update timestamps +--- -**🔧 useProfile Hook** -- ✅ Reactive state management -- ✅ Loading states -- ✅ Full CRUD -- ✅ Auto synchronization +## 📡 Endpoints -## 🏢 Company Dashboard (Frontend Features) +### Públicos -### ✅ Complete Features +| Método | Endpoint | Descrição | +|--------|----------|-----------| +| `GET` | `/health` | Health check | +| `GET` | `/swagger/*` | Documentação Swagger | +| `POST` | `/api/v1/auth/login` | Autenticação | +| `POST` | `/api/v1/companies` | Registro de empresa | +| `GET` | `/jobs` | Listar vagas | -#### 1️⃣ Job Management -**Page:** `/dashboard/empresa/vagas` -- ✅ Full listing of published jobs -- ✅ Statistics per job -- ✅ Search and filters -- ✅ Quick actions: View, Edit, Pause, Delete +### Protegidos (JWT Required) -#### 2️⃣ Application Management -**Page:** `/dashboard/empresa/candidaturas` -- ✅ View all received applications -- ✅ Statistics cards by status -- ✅ Tabs system -- ✅ Search by candidate name -- ✅ Quick actions: Approve, Reject, Email +| Método | Endpoint | Roles | Descrição | +|--------|----------|-------|-----------| +| `GET` | `/api/v1/users` | `superadmin`, `companyAdmin` | Listar usuários | +| `POST` | `/api/v1/users` | `superadmin`, `companyAdmin` | Criar usuário | +| `DELETE` | `/api/v1/users/{id}` | `superadmin` | Deletar usuário | +| `POST` | `/jobs` | `companyAdmin`, `recruiter` | Criar vaga | -#### 3️⃣ Messaging System -**Page:** `/dashboard/empresa/mensagens` -- ✅ WhatsApp/Slack style chat interface -- ✅ Conversation list with unread counters -- ✅ Real-time message attachment -- ✅ Responsive design +--- -#### 4️⃣ Analytics & Reports -**Page:** `/dashboard/empresa/relatorios` -- ✅ Key metrics cards -- ✅ Period selector -- ✅ Conversion funnel -- ✅ Hiring time by role +## 🚀 Desenvolvimento -#### 5️⃣ Company Profile -**Page:** `/dashboard/empresa/perfil` -- ✅ Real logo upload -- ✅ Basic info management -- ✅ Social media links -- ✅ Culture description +### Executar -### 🎨 Design System +```bash +# Copie o .env +cp .env.example .env -**Stack:** -- shadcn/ui -- Tailwind CSS -- Lucide Icons -- Framer Motion +# Execute +go run ./cmd/api +``` -**Colors:** -- Primary: Blue -- Success: Green -- Warning: Yellow -- Danger: Red -- Muted: Gray +### Testes + +```bash +# Todos os testes +go test ./... + +# Com verbose +go test -v ./internal/middleware/... ./internal/utils/... +``` + +### Regenerar Swagger + +```bash +swag init -g cmd/api/main.go --parseDependency --parseInternal +``` + +--- + +## 🐳 Docker + +```bash +# Build +docker build -t gohorse-backend . + +# Run +docker run -p 8080:8080 --env-file .env gohorse-backend +``` + +**Imagem otimizada:** ~73MB (Alpine + non-root user) + +--- + +## 📁 Arquivos Importantes + +| Arquivo | Descrição | +|---------|-----------| +| `cmd/api/main.go` | Entrypoint da aplicação | +| `internal/router/router.go` | Configuração de todas as rotas | +| `internal/database/database.go` | Conexão GORM com PostgreSQL | +| `migrations/*.sql` | Migrations do banco de dados | +| `docs/swagger.json` | Documentação OpenAPI gerada | diff --git a/backend/cmd/api/main.go b/backend/cmd/api/main.go index d57271d..8fb0eec 100755 --- a/backend/cmd/api/main.go +++ b/backend/cmd/api/main.go @@ -21,6 +21,15 @@ func main() { log.Println("No .env file found or error loading it") } + // Validate JWT_SECRET strength (must be at least 32 characters / 256 bits) + jwtSecret := os.Getenv("JWT_SECRET") + if jwtSecret == "" || len(jwtSecret) < 32 { + log.Println("⚠️ WARNING: JWT_SECRET is empty or too short (< 32 chars). Use a strong secret in production!") + if os.Getenv("ENV") == "production" { + log.Fatal("FATAL: Cannot start in production without strong JWT_SECRET") + } + } + database.InitDB() database.RunMigrations() diff --git a/backend/internal/README.md b/backend/internal/README.md new file mode 100644 index 0000000..74e683d --- /dev/null +++ b/backend/internal/README.md @@ -0,0 +1,86 @@ +# Internal - Backend Core + +Este diretório contém toda a lógica interna do backend, seguindo princípios de **Clean Architecture**. + +--- + +## 📁 Estrutura de Módulos + +| Diretório | Camada | Responsabilidade | +|-----------|--------|------------------| +| `api/` | Interface | Handlers e middlewares (Clean Arch) | +| `core/` | Domain | Entidades, ports e use cases (DDD) | +| `database/` | Infrastructure | Conexão GORM com PostgreSQL | +| `dto/` | Interface | Data Transfer Objects (request/response) | +| `handlers/` | Interface | Controllers HTTP (legacy) | +| `infrastructure/` | Infrastructure | Implementações de ports | +| `middleware/` | Interface | Middlewares de segurança | +| `models/` | Infrastructure | Modelos GORM | +| `router/` | Interface | Configuração de rotas | +| `services/` | Application | Lógica de negócios (legacy) | +| `utils/` | Shared | Utilitários (JWT, Sanitizer) | + +--- + +## 🏗️ Fluxo de Requisição + +``` +HTTP Request + │ + ▼ +┌─────────────┐ +│ Middleware │ (Auth, CORS, Rate Limit, Security Headers) +└─────────────┘ + │ + ▼ +┌─────────────┐ +│ Router │ (router/router.go) +└─────────────┘ + │ + ▼ +┌─────────────┐ +│ Handler │ (api/handlers/ ou handlers/) +└─────────────┘ + │ + ▼ +┌─────────────┐ +│ UseCase │ (core/usecases/) +└─────────────┘ + │ + ▼ +┌─────────────┐ +│ Repository │ (infrastructure/persistence/) +└─────────────┘ + │ + ▼ +┌─────────────┐ +│ Database │ (PostgreSQL via GORM) +└─────────────┘ +``` + +--- + +## 📦 Módulos Detalhados + +### `api/` +Implementação Clean Architecture dos handlers e middlewares. +- `handlers/` - Controllers HTTP novos +- `middleware/` - Auth com JWT Service + +### `core/` +Camada de domínio puro seguindo DDD. +- `domain/entity/` - Entidades sem dependências externas +- `ports/` - Interfaces de repositórios e serviços +- `usecases/` - Casos de uso (Login, CreateUser, etc.) + +### `middleware/` +Middlewares de segurança aplicados globalmente. +- `auth.go` - Validação JWT + RBAC +- `cors.go` - Whitelist de origens +- `rate_limit.go` - 100 req/min por IP +- `security_headers.go` - Headers OWASP + +### `utils/` +Utilitários compartilhados. +- `jwt.go` - Geração e validação de tokens +- `sanitizer.go` - Sanitização de inputs (XSS prevention) diff --git a/backend/internal/middleware/README.md b/backend/internal/middleware/README.md new file mode 100644 index 0000000..f00abfd --- /dev/null +++ b/backend/internal/middleware/README.md @@ -0,0 +1,93 @@ +# Middleware - Security Layer + +Middlewares de segurança aplicados a todas as requisições HTTP. + +--- + +## 📦 Middlewares Disponíveis + +### `auth.go` - Autenticação JWT + +Valida tokens JWT e extrai claims do usuário. + +```go +// Uso em rotas protegidas +mux.Handle("/protected", AuthMiddleware(handler)) + +// Com verificação de role +mux.Handle("/admin", AuthMiddleware(RequireRole("superadmin")(handler))) +``` + +**Claims extraídas:** +- `UserID` - ID do usuário +- `Role` - Papel (superadmin, companyAdmin, recruiter, jobSeeker) +- `CompanyID` - ID da empresa (se aplicável) + +--- + +### `cors.go` - Cross-Origin Resource Sharing + +Configura origens permitidas via variável de ambiente. + +```env +CORS_ORIGINS=http://localhost:3000,https://gohorsejobs.com +``` + +**Headers configurados:** +- `Access-Control-Allow-Origin` (whitelist) +- `Access-Control-Allow-Methods` +- `Access-Control-Allow-Headers` +- `Access-Control-Allow-Credentials` + +--- + +### `rate_limit.go` - Rate Limiting + +Limita requisições por IP para prevenir abusos. + +**Configuração padrão:** +- 100 requisições por minuto por IP +- Retorna `429 Too Many Requests` quando excedido + +**Headers de resposta:** +- `Retry-After: 60` (quando limitado) + +--- + +### `security_headers.go` - Security Headers (OWASP) + +Adiciona headers de segurança recomendados pela OWASP. + +| Header | Valor | Proteção | +|--------|-------|----------| +| `X-Frame-Options` | `DENY` | Clickjacking | +| `X-Content-Type-Options` | `nosniff` | MIME sniffing | +| `X-XSS-Protection` | `1; mode=block` | XSS | +| `Referrer-Policy` | `strict-origin-when-cross-origin` | Vazamento de referrer | +| `Content-Security-Policy` | (configurado) | Injeção de conteúdo | +| `Permissions-Policy` | (configurado) | APIs perigosas | + +--- + +### `logging.go` - Request Logging + +Loga todas as requisições com método, path e duração. + +--- + +## 🔗 Ordem de Aplicação + +Os middlewares são aplicados na seguinte ordem (de fora para dentro): + +1. **Security Headers** - Headers de segurança +2. **Rate Limiting** - Limitação de taxa +3. **CORS** - Cross-origin +4. **Logging** - Log de requisições +5. **Auth** - Autenticação (quando aplicável) + +```go +// Em router.go +handler = CORSMiddleware(handler) +handler = RateLimitMiddleware(100, time.Minute)(handler) +handler = SecurityHeadersMiddleware(handler) +``` diff --git a/backend/internal/middleware/cors.go b/backend/internal/middleware/cors.go index e96d928..6b8f828 100644 --- a/backend/internal/middleware/cors.go +++ b/backend/internal/middleware/cors.go @@ -1,16 +1,39 @@ package middleware -import "net/http" +import ( + "net/http" + "os" + "strings" +) // CORSMiddleware handles Cross-Origin Resource Sharing +// IMPORTANT: Configure CORS_ORIGINS env var in production func CORSMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - // Set CORS headers - w.Header().Set("Access-Control-Allow-Origin", "*") // TODO: Restrict in production - w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, PATCH, DELETE, OPTIONS") - w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization") + origins := os.Getenv("CORS_ORIGINS") + if origins == "" { + origins = "http://localhost:3000" + } + + origin := r.Header.Get("Origin") + allowOrigin := "" + + // Check if origin is in allowed list + for _, o := range strings.Split(origins, ",") { + if strings.TrimSpace(o) == origin { + allowOrigin = origin + break + } + } + + if allowOrigin != "" { + w.Header().Set("Access-Control-Allow-Origin", allowOrigin) + } + + w.Header().Set("Access-Control-Allow-Credentials", "true") + w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, PATCH, DELETE, OPTIONS") + w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization, X-CSRF-Token") - // Handle preflight requests if r.Method == "OPTIONS" { w.WriteHeader(http.StatusOK) return diff --git a/backend/internal/middleware/middleware_test.go b/backend/internal/middleware/middleware_test.go new file mode 100644 index 0000000..2982309 --- /dev/null +++ b/backend/internal/middleware/middleware_test.go @@ -0,0 +1,82 @@ +package middleware + +import ( + "net/http" + "net/http/httptest" + "testing" + "time" +) + +func TestRateLimiter_isAllowed(t *testing.T) { + limiter := NewRateLimiter(3, time.Minute) + + // First 3 requests should be allowed + for i := 0; i < 3; i++ { + if !limiter.isAllowed("192.168.1.1") { + t.Errorf("Request %d should be allowed", i+1) + } + } + + // 4th request should be denied + if limiter.isAllowed("192.168.1.1") { + t.Error("Request 4 should be denied") + } + + // Different IP should still be allowed + if !limiter.isAllowed("192.168.1.2") { + t.Error("Different IP should be allowed") + } +} + +func TestRateLimitMiddleware(t *testing.T) { + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + }) + + middleware := RateLimitMiddleware(2, time.Minute)(handler) + + // Create test requests + for i := 0; i < 3; i++ { + req := httptest.NewRequest("GET", "/test", nil) + req.RemoteAddr = "192.168.1.100:12345" + rr := httptest.NewRecorder() + + middleware.ServeHTTP(rr, req) + + if i < 2 { + if rr.Code != http.StatusOK { + t.Errorf("Request %d: expected status 200, got %d", i+1, rr.Code) + } + } else { + if rr.Code != http.StatusTooManyRequests { + t.Errorf("Request %d: expected status 429, got %d", i+1, rr.Code) + } + } + } +} + +func TestSecurityHeadersMiddleware(t *testing.T) { + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + }) + + middleware := SecurityHeadersMiddleware(handler) + + req := httptest.NewRequest("GET", "/test", nil) + rr := httptest.NewRecorder() + + middleware.ServeHTTP(rr, req) + + expectedHeaders := map[string]string{ + "X-Frame-Options": "DENY", + "X-Content-Type-Options": "nosniff", + "X-XSS-Protection": "1; mode=block", + } + + for header, expected := range expectedHeaders { + actual := rr.Header().Get(header) + if actual != expected { + t.Errorf("Header %s: expected %q, got %q", header, expected, actual) + } + } +} diff --git a/backend/internal/middleware/rate_limit.go b/backend/internal/middleware/rate_limit.go new file mode 100644 index 0000000..532632d --- /dev/null +++ b/backend/internal/middleware/rate_limit.go @@ -0,0 +1,117 @@ +package middleware + +import ( + "net/http" + "sync" + "time" +) + +// RateLimiter implements a simple in-memory rate limiter +type RateLimiter struct { + visitors map[string]*visitor + mu sync.RWMutex + rate int // requests allowed + window time.Duration // time window +} + +type visitor struct { + count int + lastReset time.Time +} + +// NewRateLimiter creates a rate limiter with specified requests per window +func NewRateLimiter(rate int, window time.Duration) *RateLimiter { + rl := &RateLimiter{ + visitors: make(map[string]*visitor), + rate: rate, + window: window, + } + + // Cleanup old entries periodically + go rl.cleanup() + + return rl +} + +func (rl *RateLimiter) cleanup() { + for { + time.Sleep(rl.window) + rl.mu.Lock() + for ip, v := range rl.visitors { + if time.Since(v.lastReset) > rl.window*2 { + delete(rl.visitors, ip) + } + } + rl.mu.Unlock() + } +} + +func (rl *RateLimiter) isAllowed(ip string) bool { + rl.mu.Lock() + defer rl.mu.Unlock() + + v, exists := rl.visitors[ip] + now := time.Now() + + if !exists { + rl.visitors[ip] = &visitor{count: 1, lastReset: now} + return true + } + + // Reset window if needed + if now.Sub(v.lastReset) > rl.window { + v.count = 1 + v.lastReset = now + return true + } + + if v.count >= rl.rate { + return false + } + + v.count++ + return true +} + +// getIP extracts client IP from request +func getIP(r *http.Request) string { + // Check X-Forwarded-For first (for proxied requests) + xff := r.Header.Get("X-Forwarded-For") + if xff != "" { + // Take the first IP in the chain + for i := 0; i < len(xff); i++ { + if xff[i] == ',' { + return xff[:i] + } + } + return xff + } + + // Check X-Real-IP + xri := r.Header.Get("X-Real-IP") + if xri != "" { + return xri + } + + // Fallback to RemoteAddr + return r.RemoteAddr +} + +// RateLimitMiddleware returns a middleware that limits requests per IP +func RateLimitMiddleware(rate int, window time.Duration) func(http.Handler) http.Handler { + limiter := NewRateLimiter(rate, window) + + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ip := getIP(r) + + if !limiter.isAllowed(ip) { + w.Header().Set("Retry-After", "60") + http.Error(w, "Rate limit exceeded. Please try again later.", http.StatusTooManyRequests) + return + } + + next.ServeHTTP(w, r) + }) + } +} diff --git a/backend/internal/middleware/security_headers.go b/backend/internal/middleware/security_headers.go new file mode 100644 index 0000000..a708c3d --- /dev/null +++ b/backend/internal/middleware/security_headers.go @@ -0,0 +1,32 @@ +package middleware + +import "net/http" + +// SecurityHeadersMiddleware adds essential security headers to all responses +// Based on OWASP guidelines +func SecurityHeadersMiddleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Prevent clickjacking + w.Header().Set("X-Frame-Options", "DENY") + + // Prevent MIME sniffing + w.Header().Set("X-Content-Type-Options", "nosniff") + + // Enable XSS filter + w.Header().Set("X-XSS-Protection", "1; mode=block") + + // Only send referrer for same-origin + w.Header().Set("Referrer-Policy", "strict-origin-when-cross-origin") + + // Permissions policy (disable potentially dangerous features) + w.Header().Set("Permissions-Policy", "geolocation=(), microphone=(), camera=()") + + // Content Security Policy - adjust as needed for your frontend + w.Header().Set("Content-Security-Policy", "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self'") + + // HSTS - uncomment in production with HTTPS + // w.Header().Set("Strict-Transport-Security", "max-age=31536000; includeSubDomains") + + next.ServeHTTP(w, r) + }) +} diff --git a/backend/internal/router/router.go b/backend/internal/router/router.go index 4fd9214..f8e16fc 100755 --- a/backend/internal/router/router.go +++ b/backend/internal/router/router.go @@ -3,6 +3,7 @@ package router import ( "net/http" "os" + "time" "github.com/rede5/gohorsejobs/backend/internal/api/middleware" "github.com/rede5/gohorsejobs/backend/internal/database" @@ -16,9 +17,10 @@ import ( tenantUC "github.com/rede5/gohorsejobs/backend/internal/core/usecases/tenant" userUC "github.com/rede5/gohorsejobs/backend/internal/core/usecases/user" authInfra "github.com/rede5/gohorsejobs/backend/internal/infrastructure/auth" + legacyMiddleware "github.com/rede5/gohorsejobs/backend/internal/middleware" - httpSwagger "github.com/swaggo/http-swagger/v2" _ "github.com/rede5/gohorsejobs/backend/docs" // Import generated docs + httpSwagger "github.com/swaggo/http-swagger/v2" ) func NewRouter() http.Handler { @@ -90,8 +92,12 @@ func NewRouter() http.Handler { // Swagger Route mux.HandleFunc("/swagger/", httpSwagger.WrapHandler) - // Wrap the mux with CORS middleware - handler := middleware.CORSMiddleware(mux) + // Apply middleware chain: Security Headers -> Rate Limiting -> CORS -> Router + // Order matters: outer middleware runs first + var handler http.Handler = mux + handler = middleware.CORSMiddleware(handler) + handler = legacyMiddleware.RateLimitMiddleware(100, time.Minute)(handler) // 100 req/min per IP + handler = legacyMiddleware.SecurityHeadersMiddleware(handler) return handler } diff --git a/backend/internal/utils/sanitizer.go b/backend/internal/utils/sanitizer.go new file mode 100644 index 0000000..ae70bcf --- /dev/null +++ b/backend/internal/utils/sanitizer.go @@ -0,0 +1,89 @@ +package utils + +import ( + "html" + "regexp" + "strings" + "unicode/utf8" +) + +// Sanitizer provides input sanitization utilities +type Sanitizer struct { + // Max lengths for common fields + MaxNameLength int + MaxDescriptionLength int + MaxEmailLength int +} + +// DefaultSanitizer returns a sanitizer with default settings +func DefaultSanitizer() *Sanitizer { + return &Sanitizer{ + MaxNameLength: 255, + MaxDescriptionLength: 10000, + MaxEmailLength: 320, + } +} + +// SanitizeString escapes HTML and trims whitespace +func (s *Sanitizer) SanitizeString(input string) string { + if input == "" { + return "" + } + // Trim whitespace + result := strings.TrimSpace(input) + // Escape HTML entities to prevent XSS + result = html.EscapeString(result) + return result +} + +// SanitizeName sanitizes a name field +func (s *Sanitizer) SanitizeName(input string) string { + sanitized := s.SanitizeString(input) + if utf8.RuneCountInString(sanitized) > s.MaxNameLength { + runes := []rune(sanitized) + sanitized = string(runes[:s.MaxNameLength]) + } + return sanitized +} + +// SanitizeEmail sanitizes and validates email format +func (s *Sanitizer) SanitizeEmail(input string) string { + sanitized := strings.TrimSpace(strings.ToLower(input)) + if utf8.RuneCountInString(sanitized) > s.MaxEmailLength { + return "" + } + return sanitized +} + +// SanitizeDescription sanitizes long text fields +func (s *Sanitizer) SanitizeDescription(input string) string { + sanitized := s.SanitizeString(input) + if utf8.RuneCountInString(sanitized) > s.MaxDescriptionLength { + runes := []rune(sanitized) + sanitized = string(runes[:s.MaxDescriptionLength]) + } + return sanitized +} + +// SanitizeSlug creates a URL-safe slug +func (s *Sanitizer) SanitizeSlug(input string) string { + // Convert to lowercase + result := strings.ToLower(strings.TrimSpace(input)) + // Replace spaces with hyphens + result = strings.ReplaceAll(result, " ", "-") + // Remove non-alphanumeric characters except hyphens + reg := regexp.MustCompile(`[^a-z0-9-]`) + result = reg.ReplaceAllString(result, "") + // Remove multiple consecutive hyphens + reg = regexp.MustCompile(`-+`) + result = reg.ReplaceAllString(result, "-") + // Trim hyphens from ends + result = strings.Trim(result, "-") + return result +} + +// StripHTML removes all HTML tags from input +func StripHTML(input string) string { + reg := regexp.MustCompile(`<[^>]*>`) + return reg.ReplaceAllString(input, "") +} diff --git a/backend/internal/utils/sanitizer_test.go b/backend/internal/utils/sanitizer_test.go new file mode 100644 index 0000000..7063773 --- /dev/null +++ b/backend/internal/utils/sanitizer_test.go @@ -0,0 +1,101 @@ +package utils + +import ( + "testing" +) + +func TestSanitizeString(t *testing.T) { + s := DefaultSanitizer() + + tests := []struct { + name string + input string + expected string + }{ + {"simple text", "hello world", "hello world"}, + {"with whitespace", " hello ", "hello"}, + {"with html", "", "<script>alert('xss')</script>"}, + {"empty string", "", ""}, + {"special chars", "café & thé", "café & thé"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := s.SanitizeString(tt.input) + if result != tt.expected { + t.Errorf("SanitizeString(%q) = %q, want %q", tt.input, result, tt.expected) + } + }) + } +} + +func TestSanitizeSlug(t *testing.T) { + s := DefaultSanitizer() + + tests := []struct { + name string + input string + expected string + }{ + {"simple text", "Hello World", "hello-world"}, + {"special chars", "Café & Thé!", "caf-th"}, + {"multiple spaces", "hello world", "hello-world"}, + {"already slug", "hello-world", "hello-world"}, + {"numbers", "test 123", "test-123"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := s.SanitizeSlug(tt.input) + if result != tt.expected { + t.Errorf("SanitizeSlug(%q) = %q, want %q", tt.input, result, tt.expected) + } + }) + } +} + +func TestSanitizeName(t *testing.T) { + s := DefaultSanitizer() + s.MaxNameLength = 10 + + tests := []struct { + name string + input string + expected string + }{ + {"short name", "John", "John"}, + {"max length", "1234567890", "1234567890"}, + {"over limit", "12345678901", "1234567890"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := s.SanitizeName(tt.input) + if result != tt.expected { + t.Errorf("SanitizeName(%q) = %q, want %q", tt.input, result, tt.expected) + } + }) + } +} + +func TestStripHTML(t *testing.T) { + tests := []struct { + name string + input string + expected string + }{ + {"simple html", "hello
", "hello"}, + {"script tag", "", "alert('xss')"}, + {"nested tags", "