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. +[![Go](https://img.shields.io/badge/Go-1.24-00ADD8?style=flat-square&logo=go)](https://golang.org/) +[![Next.js](https://img.shields.io/badge/Next.js-15-black?style=flat-square&logo=next.js)](https://nextjs.org/) +[![PostgreSQL](https://img.shields.io/badge/PostgreSQL-16-336791?style=flat-square&logo=postgresql)](https://postgresql.org/) +[![Docker](https://img.shields.io/badge/Docker-Ready-2496ED?style=flat-square&logo=docker)](https://docker.com/) +[![License](https://img.shields.io/badge/License-MIT-green?style=flat-square)](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. +[![Go](https://img.shields.io/badge/Go-1.24-00ADD8?style=flat-square&logo=go)](https://golang.org/) +[![PostgreSQL](https://img.shields.io/badge/PostgreSQL-16-336791?style=flat-square&logo=postgresql)](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", "
text
", "text"}, + {"no html", "plain text", "plain text"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := StripHTML(tt.input) + if result != tt.expected { + t.Errorf("StripHTML(%q) = %q, want %q", tt.input, result, tt.expected) + } + }) + } +} diff --git a/frontend/.dockerignore b/frontend/.dockerignore new file mode 100644 index 0000000..1f61406 --- /dev/null +++ b/frontend/.dockerignore @@ -0,0 +1,56 @@ +# Dependencies +node_modules/ +.pnp +.pnp.js + +# Build outputs +.next/ +out/ +build/ +dist/ + +# Testing +coverage/ +.nyc_output/ +*.lcov + +# IDE and editor +.idea/ +.vscode/ +*.swp +*.swo +*~ + +# Git +.git/ +.gitignore + +# Documentation +*.md +LICENSE + +# Environment files (security) +.env +.env.* +!.env.example + +# Debug and logs +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* +*.log + +# TypeScript cache +*.tsbuildinfo + +# OS files +.DS_Store +Thumbs.db + +# Storybook +storybook-static/ + +# Misc +*.pem +.vercel diff --git a/frontend/.stylelintrc.json b/frontend/.stylelintrc.json new file mode 100644 index 0000000..1a85c41 --- /dev/null +++ b/frontend/.stylelintrc.json @@ -0,0 +1,27 @@ +{ + "extends": ["stylelint-config-standard"], + "rules": { + "at-rule-no-unknown": [ + true, + { + "ignoreAtRules": [ + "tailwind", + "apply", + "layer", + "config", + "theme", + "custom-variant", + "import" + ] + } + ], + "function-no-unknown": [ + true, + { + "ignoreFunctions": ["theme", "oklch"] + } + ], + "import-notation": null, + "no-descending-specificity": null + } +} diff --git a/frontend/Dockerfile b/frontend/Dockerfile new file mode 100644 index 0000000..9deeee0 --- /dev/null +++ b/frontend/Dockerfile @@ -0,0 +1,68 @@ +# ============================================================================= +# GoHorse Jobs Frontend - Optimized Production Dockerfile +# ============================================================================= + +# ----------------------------------------------------------------------------- +# Stage 1: Dependencies +# ----------------------------------------------------------------------------- +FROM node:20-alpine AS deps + +WORKDIR /app + +# Install dependencies only when needed +COPY package.json package-lock.json* ./ +RUN npm ci --only=production --ignore-scripts && \ + npm cache clean --force + +# ----------------------------------------------------------------------------- +# Stage 2: Builder +# ----------------------------------------------------------------------------- +FROM node:20-alpine AS builder + +WORKDIR /app + +# Copy dependencies from deps stage +COPY --from=deps /app/node_modules ./node_modules +COPY . . + +# Build arguments for environment +ARG NEXT_PUBLIC_API_URL +ENV NEXT_PUBLIC_API_URL=$NEXT_PUBLIC_API_URL + +# Build the application +ENV NEXT_TELEMETRY_DISABLED=1 +RUN npm run build + +# ----------------------------------------------------------------------------- +# Stage 3: Production Runner +# ----------------------------------------------------------------------------- +FROM node:20-alpine AS runner + +WORKDIR /app + +# Security: Run as non-root user +RUN addgroup -g 1001 -S nodejs && \ + adduser -u 1001 -S nextjs -G nodejs + +# Set production environment +ENV NODE_ENV=production \ + NEXT_TELEMETRY_DISABLED=1 \ + PORT=3000 + +# Copy only necessary files for production +COPY --from=builder /app/public ./public +COPY --from=builder /app/.next/standalone ./ +COPY --from=builder /app/.next/static ./.next/static + +# Set ownership to non-root user +RUN chown -R nextjs:nodejs /app + +USER nextjs + +EXPOSE 3000 + +# Health check +HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \ + CMD wget -qO- http://localhost:3000 || exit 1 + +CMD ["node", "server.js"] diff --git a/frontend/README.md b/frontend/README.md index 6263cdb..543f52e 100644 --- a/frontend/README.md +++ b/frontend/README.md @@ -1,48 +1,145 @@ -# GoHorseJobs Frontend +# Frontend - GoHorse Jobs -This is the frontend for the GoHorseJobs application, built with **Next.js 15**, **Tailwind CSS**, and **shadcn/ui**. +[![Next.js](https://img.shields.io/badge/Next.js-15-black?style=flat-square&logo=next.js)](https://nextjs.org/) +[![Tailwind CSS](https://img.shields.io/badge/Tailwind-4-06B6D4?style=flat-square&logo=tailwindcss)](https://tailwindcss.com/) +[![TypeScript](https://img.shields.io/badge/TypeScript-5-3178C6?style=flat-square&logo=typescript)](https://typescriptlang.org/) -## 🚀 Getting Started +Frontend da plataforma GoHorse Jobs construído com **Next.js 15** e **App Router**. -### Prerequisites -- Node.js 18+ -- npm / yarn / pnpm +--- -### Installation +## 🏗️ Arquitetura + +``` +src/ +├── app/ # App Router (Páginas) +│ ├── dashboard/ # Área logada +│ │ ├── admin/ # Painel SuperAdmin +│ │ ├── empresa/ # Painel Empresa +│ │ └── candidato/ # Painel Candidato +│ ├── login/ # Autenticação +│ ├── vagas/ # Listagem pública +│ └── layout.tsx # Layout raiz +│ +├── components/ # Componentes React +│ ├── ui/ # shadcn/ui primitives +│ ├── forms/ # Formulários reutilizáveis +│ └── (feature)/ # Componentes por feature +│ +├── contexts/ # React Contexts +│ └── AuthContext.tsx # Autenticação global +│ +├── hooks/ # Custom Hooks +│ ├── useAuth.ts # Hook de autenticação +│ └── useProfile.ts # Hook de perfil +│ +└── lib/ # Utilitários + ├── api.ts # Cliente HTTP + └── utils.ts # Helpers gerais +``` + +--- + +## 🎨 Design System + +### Tecnologias + +| Tecnologia | Uso | +|------------|-----| +| **shadcn/ui** | Componentes base (Radix UI) | +| **Tailwind CSS 4** | Estilização utility-first | +| **Lucide Icons** | Ícones | +| **Framer Motion** | Animações | + +### Tema + +O tema está definido em `src/app/globals.css` usando CSS variables com cores `oklch()`: + +- **Primary**: Laranja (`oklch(0.68 0.22 45)`) +- **Background**: Light/Dark mode automático +- **Componentes**: Herdam do design system + +--- + +## 📱 Páginas Principais + +| Rota | Descrição | Acesso | +|------|-----------|--------| +| `/` | Landing page | Público | +| `/vagas` | Listagem de vagas | Público | +| `/login` | Autenticação | Público | +| `/dashboard/admin` | Painel admin | SuperAdmin | +| `/dashboard/empresa` | Painel empresa | CompanyAdmin, Recruiter | +| `/dashboard/candidato` | Painel candidato | JobSeeker | + +--- + +## 🚀 Desenvolvimento + +### Instalar dependências ```bash -cd frontend npm install ``` -### Running Development Server +### Executar em desenvolvimento ```bash npm run dev ``` -Open [http://localhost:3000](http://localhost:3000) with your browser. +### Build de produção -## 🛠️ Technology Stack +```bash +npm run build +npm run start +``` -- **Framework:** [Next.js 15](https://nextjs.org/) (App Router) -- **Styling:** [Tailwind CSS](https://tailwindcss.com/) -- **Components:** [shadcn/ui](https://ui.shadcn.com/) (Radix UI) -- **Icons:** [Lucide React](https://lucide.dev/) -- **Forms:** React Hook Form + Zod -- **State Management:** React Context / Hooks +### Linting -## 📂 Project Structure +```bash +npm run lint +``` -- `/src/app` - App Router pages and layouts -- `/src/components` - Reusable UI components -- `/src/components/ui` - shadcn/ui primitives -- `/src/hooks` - Custom React hooks -- `/src/lib` - Utility functions and libraries +--- -## ✨ Key Features +## 🐳 Docker -- **Company Dashboard:** Manage jobs, applications, and messages. -- **Candidate Portal:** View and apply for jobs. -- **Profile Management:** Local database integration for profile pictures. -- **Responsive Design:** Mobile-first approach. +```bash +# Build +docker build -t gohorse-frontend . + +# Run +docker run -p 3000:3000 gohorse-frontend +``` + +**Nota**: Requer `output: "standalone"` no `next.config.ts` (já configurado). + +--- + +## 📁 Componentes Principais + +| Componente | Descrição | +|------------|-----------| +| `components/ui/*` | Primitivos shadcn/ui | +| `components/dashboard-*` | Componentes do dashboard | +| `components/job-*` | Componentes de vagas | +| `components/sidebar.tsx` | Navegação lateral | + +--- + +## 🔧 Configuração + +### Variáveis de Ambiente + +```env +NEXT_PUBLIC_API_URL=http://localhost:8080 +``` + +### TypeScript + +Configurado com strict mode em `tsconfig.json`. + +### ESLint + +Configurado com `next/core-web-vitals` e `next/typescript`. diff --git a/frontend/next.config.ts b/frontend/next.config.ts index e9ffa30..f628bc1 100644 --- a/frontend/next.config.ts +++ b/frontend/next.config.ts @@ -1,7 +1,22 @@ import type { NextConfig } from "next"; const nextConfig: NextConfig = { - /* config options here */ + // Use standalone output for Docker (smaller image) + output: "standalone", + + // Performance optimizations + poweredByHeader: false, // Remove X-Powered-By header (security) + compress: true, // Enable gzip compression + + // Optional: Configure allowed image domains + images: { + remotePatterns: [ + { + protocol: "https", + hostname: "**", + }, + ], + }, }; export default nextConfig; diff --git a/frontend/src/README.md b/frontend/src/README.md new file mode 100644 index 0000000..5a27962 --- /dev/null +++ b/frontend/src/README.md @@ -0,0 +1,43 @@ +# Frontend Source + +Código fonte do frontend Next.js. + +--- + +## 📁 Estrutura + +| Diretório | Descrição | +|-----------|-----------| +| `app/` | App Router - páginas e layouts | +| `components/` | Componentes React reutilizáveis | +| `contexts/` | React Contexts (Auth, Theme) | +| `hooks/` | Custom hooks | +| `lib/` | Utilitários e configurações | + +--- + +## 🔑 Contextos + +### `AuthContext.tsx` +Gerencia autenticação global: +- Estado do usuário logado +- Funções de login/logout +- Token JWT em localStorage + +--- + +## 🪝 Hooks + +| Hook | Descrição | +|------|-----------| +| `useAuth` | Acesso ao contexto de autenticação | +| `useProfile` | Gerenciamento de perfil de usuário | + +--- + +## 📦 Lib + +| Arquivo | Descrição | +|---------|-----------| +| `utils.ts` | Funções helper (cn, formatDate) | +| `api.ts` | Cliente HTTP para backend | diff --git a/frontend/src/components/README.md b/frontend/src/components/README.md new file mode 100644 index 0000000..a9940c9 --- /dev/null +++ b/frontend/src/components/README.md @@ -0,0 +1,53 @@ +# Components + +Componentes React reutilizáveis da aplicação. + +--- + +## 📁 Estrutura + +| Diretório | Descrição | +|-----------|-----------| +| `ui/` | Primitivos shadcn/ui (Button, Card, Dialog, etc.) | +| `forms/` | Componentes de formulário | +| `*.tsx` | Componentes de feature específicos | + +--- + +## 🎨 Componentes UI (shadcn/ui) + +Componentes base do design system, instalados via: +```bash +npx shadcn-ui@latest add [component] +``` + +| Componente | Uso | +|------------|-----| +| `Button` | Botões com variantes | +| `Card` | Containers de conteúdo | +| `Dialog` | Modais | +| `Input` | Campos de texto | +| `Select` | Dropdowns | +| `Table` | Tabelas de dados | +| `Tabs` | Navegação por abas | +| `Toast` | Notificações | + +--- + +## 🧩 Componentes de Feature + +| Componente | Descrição | +|------------|-----------| +| `sidebar.tsx` | Navegação lateral do dashboard | +| `dashboard-header.tsx` | Header do dashboard | +| `job-card.tsx` | Card de vaga de emprego | +| `application-card.tsx` | Card de candidatura | + +--- + +## ✅ Convenções + +1. **Nomenclatura**: kebab-case para arquivos (`job-card.tsx`) +2. **Exports**: Named exports para componentes +3. **Props**: Interface definida no mesmo arquivo +4. **Estilização**: Tailwind CSS + cn() utility diff --git a/seeder-api/README.md b/seeder-api/README.md index a0f38dc..70605ca 100644 --- a/seeder-api/README.md +++ b/seeder-api/README.md @@ -1,116 +1,98 @@ -# Todai Jobs - Seeder API +# Seeder API -Microservice for seeding the Todai Jobs database with realistic sample data. +[![Node.js](https://img.shields.io/badge/Node.js-20-339933?style=flat-square&logo=node.js)](https://nodejs.org/) +[![PostgreSQL](https://img.shields.io/badge/PostgreSQL-16-336791?style=flat-square&logo=postgresql)](https://postgresql.org/) -## Overview +Microserviço para popular o banco de dados com dados realistas de teste. -This seeder populates the database with: -- ✅ 1 SuperAdmin user -- ✅ 10 real Japanese companies (Toyota, Honda, Denso, etc.) -- ✅ 8 Company Admins and Recruiters -- ✅ 30 Foreign Job Seekers (Brazilian, Filipino, Nepalese, etc.) -- ✅ 50 Realistic job listings -- ✅ 20 Sample applications -- ✅ 47 Japanese prefectures -- ✅ Cities for Aichi and Tokyo +--- -## Installation +## 📊 Dados Gerados + +| Entidade | Quantidade | Descrição | +|----------|------------|-----------| +| **SuperAdmin** | 1 | Usuário administrador do sistema | +| **Empresas** | 10 | Empresas reais (Toyota, Honda, etc.) | +| **Admins/Recruiters** | 8 | Usuários de empresas | +| **Candidatos** | 30 | Job seekers de várias nacionalidades | +| **Vagas** | 50 | Vagas realistas com requisitos | +| **Candidaturas** | 20 | Aplicações de exemplo | +| **Prefeituras** | 47 | Todas as prefeituras japonesas | +| **Cidades** | ~50 | Cidades de Aichi e Tokyo | + +--- + +## 🚀 Uso + +### Instalação ```bash cd seeder-api npm install ``` -## Configuration +### Configuração -Create a `.env` file or use default values: +Crie um `.env` baseado no `.env.example`: ```env -DB_HOST=172.28.22.171 +DB_HOST=localhost DB_PORT=5432 -DB_USER=usuario -DB_PASSWORD=senha123 -DB_NAME=todaijobs +DB_USER=postgres +DB_PASSWORD=yourpassword +DB_NAME=gohorsejobs ``` -## Usage +### Comandos -### Full Seed -```bash -npm run seed -``` +| Comando | Descrição | +|---------|-----------| +| `npm run seed` | Popula todas as tabelas | +| `npm run seed:reset` | Limpa e repopula tudo | +| `npm run seed:users` | Apenas usuários | +| `npm run seed:companies` | Apenas empresas | +| `npm run seed:jobs` | Apenas vagas | -### Reset Database (Drop all tables) -```bash -npm run seed:reset -``` +--- -### Seed Specific Tables -```bash -npm run seed:users -npm run seed:companies -npm run seed:jobs -``` - -## Login Credentials +## 🔑 Credenciais Geradas ### SuperAdmin -- **Identifier**: `superadmin` -- **Password**: `Admin@2025!` +- **Login**: `superadmin` +- **Senha**: `Admin@2025!` -### Company Admin (Multi-company: Toyota & Denso) -- **Identifier**: `takeshi_yamamoto` -- **Password**: `Takeshi@2025` +### Admin de Empresa +- **Login**: `takeshi_yamamoto` +- **Senha**: `Takeshi@2025` -### Recruiter (Honda) -- **Identifier**: `maria_santos` -- **Password**: `Maria@2025` +### Recrutador +- **Login**: `maria_santos` +- **Senha**: `Maria@2025` -### Job Seeker -- **Identifier**: `paulo_santos` -- **Password**: `User@2025` +### Candidato (todos usam mesma senha) +- **Login**: `paulo_santos` +- **Senha**: `User@2025` -_All job seekers use password: `User@2025`_ +--- -## Seeding Order +## 📁 Estrutura -The seeder respects foreign key dependencies: - -1. **Prefectures** (47 Japanese prefectures) -2. **Cities** (Aichi + Tokyo municipalities) -3. **Users** (SuperAdmin, Admins, Recruiters, JobSeekers) -4. **Companies** (10 major Japanese companies) -5. **UserCompany** (Multi-tenant associations) -6. **Jobs** (50 job listings) -7. **Applications** (20 sample applications) - -## Database Schema - -All tables are created via migrations in `/backend/migrations/`. This seeder only populates data. - -## Development - -The seeder uses: -- **pg**: PostgreSQL client -- **bcrypt**: Password hashing -- **dotenv**: Environment variables - -## Notes - -- Run migrations before seeding -- Use `--reset` flag carefully (destroys all data) -- Default passwords are for development only - -## Examples - -```bash -# Fresh start -npm run seed:reset -cd ../backend -psql -h 172.28.22.171 -U usuario -d todaijobs -f migrations/*.sql -cd ../seeder-api -npm run seed - -# Update data -npm run seed ``` +src/ +├── index.js # Entrypoint principal +├── config.js # Configuração de BD +└── seeders/ + ├── users.js # Seed de usuários + ├── companies.js # Seed de empresas + ├── jobs.js # Seed de vagas + ├── cities.js # Seed de cidades + └── applications.js # Seed de candidaturas +``` + +--- + +## ⚠️ Importante + +- Execute as **migrations** antes do seed +- Use apenas em ambiente de **desenvolvimento** +- Senhas padrão são **apenas para testes**