docs: overhaul global documentation with diagrams, test scenarios, and agent directives
This commit is contained in:
parent
4a096ff903
commit
028d26d2e6
6 changed files with 404 additions and 2232 deletions
409
docs/AGENTS.md
409
docs/AGENTS.md
|
|
@ -1,382 +1,103 @@
|
|||
# AGENTS.md - GoHorse Jobs
|
||||
|
||||
> **Last Updated:** 2026-02-18
|
||||
> **Purpose:** Context for AI coding assistants (Claude, Cursor, etc.)
|
||||
> **Purpose:** Context and Directives for AI coding assistants (Claude, Cursor, Aider, etc.)
|
||||
> **Branch Context:** `dev`
|
||||
|
||||
---
|
||||
|
||||
## 🚨 SUPERIOR DIRECTIVES 🚨
|
||||
|
||||
1. **DO NOT TOUCH KUBERNETES/SECRETS**: You are strictly forbidden from modifying any files in `k8s/` or altering raw RSA/base64 authentication keys.
|
||||
2. **ENV VARIABLES**: Never hardcode secrets in code. If you define a new environment variable, it must be documented. Next.js variables exposed to the client must use the `NEXT_PUBLIC_` prefix. NEVER expose secrets with `NEXT_PUBLIC_`.
|
||||
3. **READ FIRST**: Before implementing new features, always read the relevant documentation in this `docs/` folder to understand existing patterns (e.g., Go Clean Architecture, React Server Components).
|
||||
|
||||
---
|
||||
|
||||
## Project Overview
|
||||
|
||||
GoHorse Jobs is a B2B SaaS recruitment platform connecting companies with candidates. It is structured as a monorepo with multiple services sharing a single PostgreSQL database.
|
||||
GoHorse Jobs is a modern B2B SaaS recruitment platform connecting companies with candidates. It operates across multiple services sharing a single PostgreSQL database.
|
||||
|
||||
**Business model**: Freemium (Free / Pro R$199/month / Enterprise custom pricing).
|
||||
|
||||
---
|
||||
### Repository Structure
|
||||
|
||||
## Repository Structure
|
||||
|
||||
```
|
||||
```text
|
||||
gohorsejobs/
|
||||
├── backend/ # Go 1.24 REST API (Clean Architecture + DDD)
|
||||
├── frontend/ # Next.js 15 web application (App Router)
|
||||
├── backoffice/ # NestJS 11 admin/worker API (Fastify adapter)
|
||||
├── backend/ # Go REST API (Clean Architecture + DDD)
|
||||
├── frontend/ # Next.js web application (App Router)
|
||||
├── backoffice/ # NestJS admin and worker API
|
||||
├── seeder-api/ # Node.js Express database seeder
|
||||
├── job-scraper-multisite/# Job scraping service
|
||||
├── k8s/ # Kubernetes manifests (dev, hml, prd)
|
||||
├── k8s/ # Kubernetes manifests (FORBIDDEN DOMAIN)
|
||||
├── docs/ # Central documentation
|
||||
├── .forgejo/ # Forgejo CI/CD workflows
|
||||
├── .drone.yml # Drone CI/CD pipelines
|
||||
├── .forgejo/ # Forgejo actions workflows
|
||||
└── start.sh # Interactive dev startup script
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Tech Stack
|
||||
|
||||
| Service | Language | Framework | Key Libraries |
|
||||
|---------|----------|-----------|---------------|
|
||||
| Backend | Go 1.24 | stdlib net/http | JWT v5, lib/pq, GORM, Stripe, AWS SDK v2, RabbitMQ, Firebase Admin, Swaggo |
|
||||
| Frontend | TypeScript 5 | Next.js 15 (App Router) | React 19, Tailwind CSS 4, shadcn/ui, Zustand, React Hook Form + Zod, Framer Motion |
|
||||
| Backoffice | TypeScript 5 | NestJS 11 (Fastify) | TypeORM, Passport + JWT, Stripe, AMQP, Nodemailer, Pino, Firebase Admin |
|
||||
| Seeder | JavaScript (ESM) | Express 5 | pg, bcrypt |
|
||||
|
||||
**Database**: PostgreSQL 16+ with UUID v7 primary keys. 43+ SQL migration files in `backend/migrations/`.
|
||||
|
||||
---
|
||||
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
./start.sh # Interactive menu
|
||||
```
|
||||
|
||||
| Option | Action |
|
||||
|--------|--------|
|
||||
| 1 | Start Frontend + Backend |
|
||||
| 2 | Reset DB + Seed + Start |
|
||||
| 3 | Start all services (Frontend + Backend + Backoffice) |
|
||||
| 4 | Run migrations only |
|
||||
| 5 | Seed database (append) |
|
||||
| 6 | Full DB reset + migrate + seed |
|
||||
| 7 | Run backend E2E tests |
|
||||
| 8 | Seed reset LITE (skip 153k cities) |
|
||||
| 9 | Run all tests (Backend + Frontend) |
|
||||
|
||||
### Service Ports
|
||||
|
||||
| Service | Port |
|
||||
|---------|------|
|
||||
| Backend | 8521 |
|
||||
| Frontend | 8963 |
|
||||
| Backoffice | 3001 |
|
||||
| Swagger (Backend) | 8521/swagger/index.html |
|
||||
| Swagger (Backoffice) | 3001/api/docs |
|
||||
|
||||
---
|
||||
|
||||
## Build & Test Commands
|
||||
|
||||
### Backend (Go)
|
||||
|
||||
```bash
|
||||
cd backend && go run cmd/api/main.go # Run
|
||||
go test -v ./... -count=1 # All unit tests
|
||||
go test -tags=e2e -v ./tests/e2e/... # E2E tests
|
||||
swag init -g cmd/api/main.go --parseDependency --parseInternal # Swagger
|
||||
go mod tidy # Dependencies
|
||||
```
|
||||
|
||||
### Frontend (Next.js)
|
||||
|
||||
```bash
|
||||
cd frontend
|
||||
npm install # Install deps
|
||||
npm run dev # Dev server (use -p 8963 for project convention)
|
||||
npm run build # Production build
|
||||
npm run lint # ESLint
|
||||
npm run test # Jest unit tests
|
||||
```
|
||||
|
||||
### Backoffice (NestJS)
|
||||
|
||||
```bash
|
||||
cd backoffice
|
||||
pnpm install # Uses pnpm
|
||||
npm run start:dev # Dev server with watch
|
||||
npm run build # Production build
|
||||
npm run lint # ESLint with auto-fix
|
||||
npm run test # Jest unit tests
|
||||
npm run test:e2e # E2E tests
|
||||
```
|
||||
|
||||
### Seeder
|
||||
|
||||
```bash
|
||||
cd seeder-api
|
||||
npm install
|
||||
npm run migrate # Run migrations
|
||||
npm run seed # Seed data
|
||||
npm run seed:reset # Drop all tables
|
||||
npm run seed:lite # Seed without 153k cities
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Architecture & Conventions
|
||||
|
||||
### Backend (Go) - Clean Architecture + DDD
|
||||
### Backend (Go 1.24)
|
||||
* **Architecture**: Clean Architecture + Domain Driven Design (DDD)
|
||||
* **Flow**: Controller (`internal/handlers`) -> UseCase (`internal/core/usecases`) -> Interface Port (`internal/core/ports`) -> Repository (`internal/infrastructure/persistence`).
|
||||
* **Key Libs**: `net/http` (stdlib routing), `lib/pq` + `gorm` (legacy/transitioning), `swaggo` (Docs).
|
||||
* **Running**: `cd backend && go run cmd/api/main.go` (Port 8521)
|
||||
|
||||
```
|
||||
backend/internal/
|
||||
├── api/ # (legacy) HTTP handlers
|
||||
├── handlers/ # HTTP request handlers (current)
|
||||
├── middleware/ # Auth, CORS, rate limiting, security headers
|
||||
├── core/
|
||||
│ ├── domain/entity/ # Business entities (User, Company, Job, etc.)
|
||||
│ ├── ports/ # Repository interfaces
|
||||
│ └── usecases/ # Business logic (LoginUseCase, RegisterUseCase, etc.)
|
||||
├── infrastructure/
|
||||
│ ├── auth/ # JWT service
|
||||
│ ├── persistence/ # PostgreSQL repository implementations
|
||||
│ └── storage/ # S3/R2 storage adapter
|
||||
├── services/ # Business services (Email, FCM, Storage, Admin)
|
||||
├── dto/ # Data Transfer Objects
|
||||
├── router/ # Route definitions
|
||||
├── models/ # GORM models (legacy, being migrated)
|
||||
├── database/ # DB connection & migration runner
|
||||
└── utils/ # Utilities (JWT, sanitizer)
|
||||
```
|
||||
### Frontend (Next.js 15)
|
||||
* **Architecture**: App Router (`src/app`), heavily favoring React Server Components (RSC). Use `'use client'` strictly at the leaf nodes of the component tree for interactivity.
|
||||
* **Styling**: Tailwind CSS, generic UI components in `src/components/ui` (shadcn).
|
||||
* **State & Validation**: Zustand (global state), React Hook Form + Zod (forms).
|
||||
* **i18n**: Multi-language support (PT, EN, ES, JA). Ensure all text is extracted to translation files.
|
||||
* **Running**: `cd frontend && npm run dev -p 8963` (Port 8963)
|
||||
|
||||
**Patterns**:
|
||||
- Constructor injection: `func NewService(db *sql.DB) *Service`
|
||||
- All DB operations accept `ctx context.Context`
|
||||
- Error handling: `(T, error)` return tuples
|
||||
- Repository interfaces in `core/ports/`, implementations in `infrastructure/persistence/`
|
||||
- Test files: `*_test.go`
|
||||
### Backoffice (NestJS 11)
|
||||
* **Architecture**: Modular NestJS over Fastify.
|
||||
* **Modules**: Admin, Auth, Email (Worker), Stripe, Tickets.
|
||||
* **Running**: `cd backoffice && npm run start:dev` (Port 3001)
|
||||
|
||||
### Frontend (Next.js) - App Router
|
||||
|
||||
```
|
||||
frontend/src/
|
||||
├── app/ # File-based routing (20+ routes)
|
||||
│ ├── dashboard/ # Protected routes (12+ sub-pages)
|
||||
│ ├── jobs/ # Job listing & details
|
||||
│ └── auth/ # Login, register flows
|
||||
├── components/ # Reusable components (44+)
|
||||
│ └── ui/ # shadcn/ui primitives (24+)
|
||||
├── hooks/ # Custom React hooks
|
||||
├── contexts/ # React contexts (auth, theme)
|
||||
├── lib/ # Utilities (API calls, validation)
|
||||
└── i18n/ # Internationalization (PT, EN, ES, JA)
|
||||
```
|
||||
|
||||
**Patterns**:
|
||||
- Server Components by default; use `'use client'` for interactivity
|
||||
- Tailwind CSS utility classes
|
||||
- Path alias: `@/*` maps to `./src/*`
|
||||
- Form validation: React Hook Form + Zod schemas
|
||||
- Global state: Zustand stores
|
||||
- Test files: `*.test.tsx`
|
||||
|
||||
### Backoffice (NestJS) - Modular
|
||||
|
||||
```
|
||||
backoffice/src/
|
||||
├── admin/ # Dashboard & statistics
|
||||
├── auth/ # JWT authentication (Passport)
|
||||
├── email/ # Email worker (AMQP/LavinMQ consumer)
|
||||
├── external-services/ # Credential management
|
||||
├── fcm-tokens/ # Firebase push tokens
|
||||
├── plans/ # Subscription plans
|
||||
├── stripe/ # Stripe payment integration
|
||||
└── tickets/ # Support tickets
|
||||
```
|
||||
|
||||
**Patterns**:
|
||||
- NestJS module pattern: `*.module.ts`, `*.controller.ts`, `*.service.ts`
|
||||
- Guards: `@UseGuards(JwtAuthGuard)`
|
||||
- DTOs: `create-*.dto.ts`, `update-*.dto.ts` with class-validator
|
||||
- Prettier: single quotes, trailing commas
|
||||
### Seeder API (Express 5)
|
||||
* **Purpose**: Manages database migrations and test data populations.
|
||||
* **Running**: `cd seeder-api && npm run seed`
|
||||
|
||||
---
|
||||
|
||||
## Authentication & Authorization
|
||||
## ⚠️ Known Gotchas & Historical Errors
|
||||
|
||||
- **JWT**: HS256 with HttpOnly cookies (web) + Bearer tokens (API/mobile)
|
||||
- **4 roles**: `superadmin` > `admin` > `recruiter` > `candidate`
|
||||
- **Middleware stack**: Auth (JWT+RBAC) -> CORS -> Rate Limiting (100 req/min) -> Security Headers -> XSS Sanitizer
|
||||
- **JWT secret must match** between Backend and Backoffice
|
||||
- **Test credentials**: See [TEST_USERS.md](TEST_USERS.md) for all test accounts
|
||||
### 1. The `PASSWORD_PEPPER` Trap
|
||||
**Symptom**: "Invalid credentials" upon login right after seeding the database.
|
||||
**Cause**: The initial SQL migration creates the superadmin, but lacks the bcrypt pepper hashing. The Node.js `seeder-api` calculates the real hash.
|
||||
**Fix**: You must run `npm run migrate` **AND THEN** `npm run seed` in the `seeder-api` directory. The seeder will read `PASSWORD_PEPPER` (e.g., `gohorse-pepper`) from your `.env` and fix the hashes.
|
||||
|
||||
### 2. Login Payload
|
||||
The frontend login form sends `{ "email": "admin", "password": "..." }`. The backend resolves this against the `email` or `identifier` columns. **Do not** change the payload payload to `{ "identifier": "admin" }` because the backend DTO explicitly expects the JSON key `"email"`.
|
||||
|
||||
### 3. Bcrypt in bash strings
|
||||
**Never** run a raw SQL command via bash `docker exec` containing a bcrypt hash like `$2b$10$...`. The bash shell expands `$2b` as a variable, corrupting the hash. Always write the SQL to a file and execute the file.
|
||||
|
||||
### 4. Contact/Tickets
|
||||
The contact form uses Next.js internationalization and routes directly to the backoffice `tickets` API. Emails go to `wtf@gohorsejobs.com`.
|
||||
|
||||
---
|
||||
|
||||
## Database
|
||||
## Dev Environments & Test Users
|
||||
|
||||
- PostgreSQL 16+ with UUID v7 for primary keys (SERIAL for reference tables)
|
||||
- Migrations: `backend/migrations/` (43+ SQL files, numbered `000_` through `999_`)
|
||||
- Core tables: `users`, `companies`, `user_companies`, `jobs`, `applications`, `favorite_jobs`, `notifications`, `tickets`, `activity_logs`, `job_payments`
|
||||
**Environments**:
|
||||
* **dev**: Current working branch. Deployed to `dev.gohorsejobs.com` or `local.gohorsejobs.com`
|
||||
* **main**: Production branch.
|
||||
|
||||
---
|
||||
**Test Users** (Local/Dev):
|
||||
* Superadmin: `lol` / `Admin@2025!`
|
||||
* Superadmin: `superadmin` / `Admin@2025!`
|
||||
|
||||
## API Routes
|
||||
|
||||
All backend routes under `/api/v1`:
|
||||
- **Auth**: `/auth/login`, `/auth/register/candidate`, `/auth/register/company`, `/auth/forgot-password`, `/auth/reset-password`
|
||||
- **Jobs**: CRUD at `/jobs`, moderation at `/jobs/moderation`
|
||||
- **Companies**: CRUD at `/companies`, status management
|
||||
- **Users**: `/users/me` (profile), admin CRUD at `/users`
|
||||
- **Applications**: `/applications` with status updates
|
||||
- **Storage**: `/storage/upload-url` (presigned S3/R2 URLs)
|
||||
- **Admin**: `/admin/companies`, `/admin/email-templates`, `/admin/email-settings`
|
||||
- **Notifications**: `/notifications`, `/tokens` (FCM)
|
||||
- **Chat**: `/conversations`, `/conversations/{id}/messages`
|
||||
|
||||
---
|
||||
|
||||
## External Services
|
||||
|
||||
| Service | Purpose |
|
||||
|---------|---------|
|
||||
| Stripe | Payment processing & subscriptions |
|
||||
| Firebase (FCM) | Push notifications |
|
||||
| Appwrite | Real-time chat/messaging |
|
||||
| LavinMQ (AMQP) | Message queue for background jobs |
|
||||
| Cloudflare R2 / S3 | File/image storage |
|
||||
| Resend | Transactional email |
|
||||
|
||||
---
|
||||
|
||||
## Deployment
|
||||
|
||||
- **Environments**: `dev` (branch: dev), `hml` (branch: hml), `prd` (branch: main)
|
||||
- **CI/CD**: Forgejo workflows (`.forgejo/workflows/`) + Drone (`.drone.yml`)
|
||||
- **Container runtime**: Podman (Apolo), Coolify/Docker (Redbull), Kubernetes (production)
|
||||
- **Registry**: Forgejo (`forgejo-gru.rede5.com.br/rede5/`)
|
||||
- **Coolify UI**: https://redbull.rede5.com.br (Redbull VPS DEV environment)
|
||||
|
||||
### DEV Environments
|
||||
|
||||
| Server | Type | URLs |
|
||||
|--------|------|------|
|
||||
| **Redbull** (Coolify) | Auto-deploy via Git | `local.gohorsejobs.com`, `api-local.gohorsejobs.com` |
|
||||
| **Apolo** (Podman/Quadlet) | Manual deploy | `dev.gohorsejobs.com`, `api-tmp.gohorsejobs.com` |
|
||||
|
||||
---
|
||||
|
||||
## Git Workflow
|
||||
|
||||
### Remotes
|
||||
|
||||
| Remote | URL | Function |
|
||||
|--------|-----|----------|
|
||||
| **origin** | git@github.com:rede5/gohorsejobs.git | GitHub - main development |
|
||||
| **pipe** | https://pipe.gohorsejobs.com/bohessefm/gohorsejobs.git | Forgejo - mirror/CI |
|
||||
|
||||
### Branches
|
||||
|
||||
| Branch | Environment | Description |
|
||||
|--------|-------------|-------------|
|
||||
| **dev** | Development | Main working branch |
|
||||
| **hml** | Staging | Pre-production testing |
|
||||
| **main** | Production | Stable release |
|
||||
|
||||
### Sync Flow
|
||||
|
||||
```bash
|
||||
# 1. Pull from GitHub
|
||||
git pull origin dev
|
||||
|
||||
# 2. Push to Forgejo (pipe)
|
||||
git push pipe dev
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Key Things to Know
|
||||
|
||||
1. Backend uses Clean Architecture + DDD; respect layer boundaries (handlers -> usecases -> ports/repositories)
|
||||
2. Frontend uses Next.js App Router; pages in `src/app/`, components in `src/components/`
|
||||
3. Backoffice shares the same PostgreSQL database as the backend
|
||||
4. Migrations are plain SQL files, not managed by an ORM
|
||||
5. Project supports 4 languages (PT, EN, ES, JA) via i18n
|
||||
6. Environment variables in `.env` files (gitignored)
|
||||
7. Use `start.sh` for local development
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ Known Gotchas
|
||||
|
||||
### 1. PASSWORD_PEPPER e o hash do superadmin são gerados em runtime
|
||||
|
||||
A migration `010_seed_super_admin.sql` **não hardcoda hash**. Ela cria o superadmin com um
|
||||
placeholder inválido e `force_change_password`. O **seeder-api** é quem gera o hash correto
|
||||
em runtime lendo `process.env.PASSWORD_PEPPER`.
|
||||
|
||||
Fluxo correto após reset do banco:
|
||||
```
|
||||
npm run migrate → superadmin criado, hash = placeholder (não faz login)
|
||||
npm run seed → hash recalculado com pepper do ambiente, status = active
|
||||
```
|
||||
|
||||
O `start.sh` opções 2, 6 e 8 já fazem isso automaticamente (migrate → seed).
|
||||
|
||||
| Location | `PASSWORD_PEPPER` |
|
||||
|----------|-------|
|
||||
| `backend/.env` (local) | `gohorse-pepper` |
|
||||
| `seeder-api/.env` | `gohorse-pepper` |
|
||||
| Coolify DEV (`iw4sow8s0kkg4cccsk08gsoo`) | `gohorse-pepper` |
|
||||
|
||||
Se `PASSWORD_PEPPER` mudar, basta rodar `npm run seed` novamente — o hash é recalculado
|
||||
automaticamente. Nenhuma migration precisa ser alterada.
|
||||
|
||||
If you suspect a mismatch, see the full fix procedure in [DEVOPS.md](DEVOPS.md#troubleshooting-login-retorna-invalid-credentials).
|
||||
|
||||
### 2. Login API field: `email`, not `identifier`
|
||||
|
||||
The frontend login form sends `{ "email": "<username_or_email>", "password": "..." }`.
|
||||
The backend core DTO (`internal/core/dto/user_auth.go`) has field `Email`, **not** `Identifier`.
|
||||
The repository resolves it via `WHERE email = $1 OR identifier = $1`, so both username and email work.
|
||||
|
||||
Do **not** send `{ "identifier": "..." }` directly to `/api/v1/auth/login` — the field will be ignored and login will fail silently.
|
||||
|
||||
### 3. Bcrypt hashes in SQL — always use a file, never `-c`
|
||||
|
||||
Shell interprets `$` in bcrypt hashes as variable expansions. Example of **broken** approach:
|
||||
```bash
|
||||
# ❌ WRONG — $2b gets expanded by shell, hash gets corrupted
|
||||
docker exec postgres psql -U user -d db -c "UPDATE users SET password_hash='$2b$10$abc...'"
|
||||
```
|
||||
Correct approach — write to file first, then use `-f`:
|
||||
```bash
|
||||
cat > /tmp/fix.sql <<'EOF'
|
||||
UPDATE users SET password_hash = '$2b$10$4759wJhnXnBpcwSnVZm9Eu.wTqGYVCHkxAU5a2NxhsFHU42nV3tzW' WHERE identifier = 'lol';
|
||||
EOF
|
||||
docker cp /tmp/fix.sql <postgres_container>:/tmp/fix.sql
|
||||
docker exec <postgres_container> psql -U gohorsejobs -d gohorsejobs -f /tmp/fix.sql
|
||||
```
|
||||
|
||||
### 4. Test credentials (DEV/local.gohorsejobs.com)
|
||||
|
||||
| Role | Identifier | Password | Email |
|
||||
|------|-----------|----------|-------|
|
||||
| superadmin | `lol` | `Admin@2025!` | lol@gohorsejobs.com |
|
||||
| superadmin | `superadmin` | `Admin@2025!` | admin@gohorsejobs.com |
|
||||
|
||||
---
|
||||
See [docs/TEST_USERS.md](TEST_USERS.md) for Candidate and Company personas.
|
||||
|
||||
## Documentation Index
|
||||
|
||||
| Document | Location | Description |
|
||||
|----------|----------|-------------|
|
||||
| API Reference | [API.md](API.md) | Endpoints, contracts, examples |
|
||||
| API Security | [API_SECURITY.md](API_SECURITY.md) | Auth, RBAC, permissions |
|
||||
| Database Schema | [DATABASE.md](DATABASE.md) | Tables, ERD, migrations |
|
||||
| DevOps | [DEVOPS.md](DEVOPS.md) | Infrastructure, deployment, diagrams |
|
||||
| Test Users | [TEST_USERS.md](TEST_USERS.md) | Credenciais de teste por role |
|
||||
| Roadmap | [ROADMAP.md](ROADMAP.md) | Product direction |
|
||||
| Tasks | [TASKS.md](TASKS.md) | Task tracking |
|
||||
| Workflows | [WORKFLOWS.md](WORKFLOWS.md) | Deployment workflows |
|
||||
| Backend | [../backend/BACKEND.md](../backend/BACKEND.md) | Go API details |
|
||||
| Frontend | [../frontend/FRONTEND.md](../frontend/FRONTEND.md) | Next.js details |
|
||||
| Backoffice | [../backoffice/BACKOFFICE.md](../backoffice/BACKOFFICE.md) | NestJS details |
|
||||
| Seeder | [../seeder-api/SEEDER-API.md](../seeder-api/SEEDER-API.md) | Database seeding & test data |
|
||||
When asked to learn or modify parts of the system, consult these files first:
|
||||
* [API.md](API.md) - Endpoint contracts
|
||||
* [API_SECURITY.md](API_SECURITY.md) - Auth, RBAC
|
||||
* [APPSEC_STRATEGY.md](APPSEC_STRATEGY.md) - Application Security and Testing
|
||||
* [DATABASE.md](DATABASE.md) - Schema and ERD
|
||||
* [DEVOPS.md](DEVOPS.md) - Cloudflare, Traefik, Containers
|
||||
* [TASKS.md](TASKS.md) / [TEST_USERS.md](TEST_USERS.md)
|
||||
|
|
|
|||
611
docs/API.md
611
docs/API.md
|
|
@ -1,574 +1,87 @@
|
|||
# 📡 GoHorse Jobs - API Documentation
|
||||
# 🔌 API Documentation & Routing - GoHorseJobs
|
||||
|
||||
Complete API reference with routes, permissions, and modules.
|
||||
|
||||
> **Last Updated:** 2026-02-16
|
||||
> **Base URL:** `https://api.gohorsejobs.com/api/v1`
|
||||
> **Auth:** JWT Bearer Token or HttpOnly Cookie
|
||||
This document outlines the API landscape across the GoHorseJobs monorepo, detailing the main services and how they communicate.
|
||||
|
||||
---
|
||||
|
||||
## 🔐 Authentication
|
||||
## 🏗️ API Architecture & Communication
|
||||
|
||||
### Methods
|
||||
1. **Authorization Header:** `Authorization: Bearer <token>`
|
||||
2. **Cookie:** `jwt=<token>` (HttpOnly, Secure)
|
||||
* **Backend API (Go):** The central source of truth for the platform. Handles all core business logic (Users, Companies, Jobs, Applications).
|
||||
* **Backoffice API (NestJS):** Dedicated to administrative tasks, workers (email queues), and specialized integrations (Stripe, Firebase Cloud Messaging). Shares the **same PostgreSQL database** as the Backend API.
|
||||
* **Seeder API (Node.js):** Runs strictly on demand to populate the database with dummy data or run migrations.
|
||||
|
||||
### Roles
|
||||
| Role | Code | Level | Description |
|
||||
|------|------|-------|-------------|
|
||||
| **SuperAdmin** | `superadmin` | 0 | Platform administrator |
|
||||
| **Admin** | `admin` | 1 | Company administrator |
|
||||
| **Recruiter** | `recruiter` | 2 | Job poster |
|
||||
| **Candidate** | `candidate` | 3 | Candidate |
|
||||
| **Guest** | - | - | No authentication |
|
||||
### Communication Flow (Frontend)
|
||||
The Next.js Frontend primarily communicates with the **Backend API**.
|
||||
* **Paradigm:** The frontend uses React Server Components (RSC) to fetch data natively from the Go API.
|
||||
* **Client Actions:** For forms and interactivity (e.g., login, apply), client components dispatch requests using the wrapped fetch utility located at `src/lib/api.ts`.
|
||||
* **Auth State:** JWT tokens are stored locally (Cookies/Storage) and appended to the `Authorization: Bearer <token>` header manually via interceptors or utility functions.
|
||||
|
||||
---
|
||||
|
||||
## 📋 Module: Authentication
|
||||
## 📡 Backend API Reference (`/api/v1`)
|
||||
Hosted at `api-local.gohorsejobs.com` (Dev) or `api.gohorsejobs.com` (Prd).
|
||||
View detailed interactive Swagger Docs by visiting `/swagger/index.html`.
|
||||
|
||||
### Login
|
||||
```http
|
||||
POST /api/v1/auth/login
|
||||
```
|
||||
| Field | Auth | Roles | Description |
|
||||
|-------|------|-------|-------------|
|
||||
| Public | ❌ | Guest | Authenticate user |
|
||||
### Authentication
|
||||
* `POST /auth/login`: Accepts `{ "email": "...", "password": "..."}`. Returns JWT token and User DTO.
|
||||
* `POST /auth/register/candidate`: Creates a standard user.
|
||||
* `POST /auth/register/company`: Creates a user and a linked `Company` profile awaiting approval.
|
||||
|
||||
**Request:**
|
||||
```json
|
||||
{
|
||||
"identifier": "lol",
|
||||
"password": "Admin@2025!"
|
||||
}
|
||||
```
|
||||
### Users & Profiles
|
||||
* `GET /users/me`: Gets the full profile of the currently authenticated user (derived from JWT context).
|
||||
* `PUT /users/me`: Updates profile details.
|
||||
* `GET /admin/users`: (Admin only) Lists all users.
|
||||
|
||||
**Response (200):**
|
||||
```json
|
||||
{
|
||||
"token": "eyJhbGciOiJI...",
|
||||
"user": { "id": 1, "role": "superadmin", "name": "Dr. Horse Expert" }
|
||||
}
|
||||
```
|
||||
### Companies
|
||||
* `GET /companies`: Lists active companies with pagination and filters.
|
||||
* `GET /companies/{id}`: Detailed company view.
|
||||
* `PUT /companies/{id}`: Edit company profile (Requires owner/admin permissions via `user_companies` relation).
|
||||
* `POST /admin/companies/{id}/status`: (Admin only) Approve/Reject pending companies.
|
||||
|
||||
### Jobs & Applications
|
||||
* `GET /jobs`: Lists published jobs. (Supports full-text search and filters).
|
||||
* `POST /jobs`: Creates a draft job (Requires Recruiter).
|
||||
* `PUT /jobs/{id}`: Updates job details.
|
||||
* `POST /jobs/{id}/apply`: Submits an application for the job.
|
||||
* `GET /applications`: Lists received applications (for recruiters) or submitted applications (for candidates).
|
||||
* `POST /applications/{id}/status`: Updates application status (`reviewing`, `accepted`, `rejected`).
|
||||
|
||||
### Miscellaneous
|
||||
* `GET /health`: Basic ping endpoint.
|
||||
* `POST /storage/upload-url`: Requests a presigned URL (S3/Cloudflare R2) to upload logos or resumes directly from the browser.
|
||||
|
||||
---
|
||||
|
||||
### Register Candidate
|
||||
```http
|
||||
POST /api/v1/auth/register
|
||||
```
|
||||
| Field | Auth | Roles | Description |
|
||||
|-------|------|-------|-------------|
|
||||
| Public | ❌ | Guest | Register new job seeker |
|
||||
## 🛠️ Backoffice API Reference (`/api`)
|
||||
Hosted at `b-local.gohorsejobs.com` (Dev) or `b.gohorsejobs.com` (Prd).
|
||||
View detailed interactive Swagger Docs by visiting `/api/docs`.
|
||||
|
||||
**Request:**
|
||||
```json
|
||||
{
|
||||
"name": "John Doe",
|
||||
"email": "john@example.com",
|
||||
"password": "SecurePass123!"
|
||||
}
|
||||
```
|
||||
### Tickets & Support
|
||||
* `POST /tickets`: Public endpoint (no auth required) to submit a contact/support ticket. Used directly by the frontend `/contact` page.
|
||||
* `GET /tickets`: (Admin only) Lists all open tickets.
|
||||
* `POST /tickets/{id}/reply`: (Admin only) Adds a message to a ticket and triggers an email notification to the user.
|
||||
|
||||
### Emails & Workers
|
||||
The Backoffice listens to a LavinMQ (AMQP) message queue to dispatch transactional emails (welcome, password reset, application updates) securely without blocking the main Go runtime.
|
||||
|
||||
### Stripe Billing
|
||||
* `POST /stripe/webhook`: Consumes Stripe events to upgrade company plans from Free to Pro/Enterprise.
|
||||
|
||||
---
|
||||
|
||||
## 🏢 Module: Companies
|
||||
## 📚 Seeder API Commands
|
||||
|
||||
### Create Company
|
||||
```http
|
||||
POST /api/v1/companies
|
||||
```
|
||||
| Field | Auth | Roles | Description |
|
||||
|-------|------|-------|-------------|
|
||||
| Public | ❌ | Guest | Register new company |
|
||||
If you need to instantly provision the database for these routes to work:
|
||||
|
||||
### List Companies
|
||||
```http
|
||||
GET /api/v1/companies
|
||||
```
|
||||
| Field | Auth | Roles | Description |
|
||||
|-------|------|-------|-------------|
|
||||
| Smart | ⚪ Optional | Guest/Admin | Public list or admin full list |
|
||||
```bash
|
||||
cd seeder-api
|
||||
npm install
|
||||
|
||||
### Update Company Status
|
||||
```http
|
||||
PATCH /api/v1/companies/{id}/status
|
||||
```
|
||||
| Field | Auth | Roles | Description |
|
||||
|-------|------|-------|-------------|
|
||||
| Protected | ✅ | superadmin, admin | Activate/deactivate company |
|
||||
# Drops all tables, runs migrations, inserts core test users (100 jobs, 50 companies)
|
||||
npm run seed:lite
|
||||
|
||||
---
|
||||
|
||||
## 👥 Module: Users
|
||||
|
||||
### Get Current User
|
||||
```http
|
||||
GET /api/v1/users/me
|
||||
```
|
||||
| Field | Auth | Roles | Description |
|
||||
|-------|------|-------|-------------|
|
||||
| Protected | ✅ | Any authenticated | Get current user profile |
|
||||
|
||||
### List Users
|
||||
```http
|
||||
GET /api/v1/users
|
||||
```
|
||||
| Field | Auth | Roles | Description |
|
||||
|-------|------|-------|-------------|
|
||||
| Protected | ✅ | superadmin, admin | List all users |
|
||||
|
||||
### Create User
|
||||
```http
|
||||
POST /api/v1/users
|
||||
```
|
||||
| Field | Auth | Roles | Description |
|
||||
|-------|------|-------|-------------|
|
||||
| Protected | ✅ | superadmin, admin | Create new user |
|
||||
|
||||
### Update User
|
||||
```http
|
||||
PATCH /api/v1/users/{id}
|
||||
```
|
||||
| Field | Auth | Roles | Description |
|
||||
|-------|------|-------|-------------|
|
||||
| Protected | ✅ | Any authenticated | Update user profile |
|
||||
|
||||
### Delete User
|
||||
```http
|
||||
DELETE /api/v1/users/{id}
|
||||
```
|
||||
| Field | Auth | Roles | Description |
|
||||
|-------|------|-------|-------------|
|
||||
| Protected | ✅ | superadmin | Delete user |
|
||||
|
||||
### List User Roles
|
||||
```http
|
||||
GET /api/v1/users/roles
|
||||
```
|
||||
| Field | Auth | Roles | Description |
|
||||
|-------|------|-------|-------------|
|
||||
| Protected | ✅ | superadmin, admin | List available roles |
|
||||
|
||||
---
|
||||
|
||||
## 💼 Module: Jobs
|
||||
|
||||
### List Jobs (Public)
|
||||
```http
|
||||
GET /api/v1/jobs
|
||||
```
|
||||
| Field | Auth | Roles | Description |
|
||||
|-------|------|-------|-------------|
|
||||
| Public | ❌ | Guest | Search and filter jobs |
|
||||
|
||||
**Query Parameters:**
|
||||
| Param | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `q` | string | Search query (title, description) |
|
||||
| `location` | string | Filter by location |
|
||||
| `type` | string | Employment type filter |
|
||||
| `workMode` | string | `onsite`, `hybrid`, `remote` |
|
||||
| `page` | int | Page number (default: 1) |
|
||||
| `limit` | int | Items per page (default: 20) |
|
||||
|
||||
### Get Job by ID
|
||||
```http
|
||||
GET /api/v1/jobs/{id}
|
||||
```
|
||||
| Field | Auth | Roles | Description |
|
||||
|-------|------|-------|-------------|
|
||||
| Public | ❌ | Guest | Get job details |
|
||||
|
||||
### Create Job
|
||||
```http
|
||||
POST /api/v1/jobs
|
||||
```
|
||||
| Field | Auth | Roles | Description |
|
||||
|-------|------|-------|-------------|
|
||||
| Public* | ❌ | admin, recruiter | Create new job posting |
|
||||
|
||||
### Update Job
|
||||
```http
|
||||
PUT /api/v1/jobs/{id}
|
||||
```
|
||||
| Field | Auth | Roles | Description |
|
||||
|-------|------|-------|-------------|
|
||||
| Public* | ❌ | admin, recruiter | Update job posting |
|
||||
|
||||
### Delete Job
|
||||
```http
|
||||
DELETE /api/v1/jobs/{id}
|
||||
```
|
||||
| Field | Auth | Roles | Description |
|
||||
|-------|------|-------|-------------|
|
||||
| Public* | ❌ | admin, recruiter | Delete job posting |
|
||||
|
||||
### List Jobs for Moderation
|
||||
```http
|
||||
GET /api/v1/jobs/moderation
|
||||
```
|
||||
| Field | Auth | Roles | Description |
|
||||
|-------|------|-------|-------------|
|
||||
| Protected | ✅ | superadmin, admin | List all jobs for moderation |
|
||||
|
||||
### Update Job Status
|
||||
```http
|
||||
PATCH /api/v1/jobs/{id}/status
|
||||
```
|
||||
| Field | Auth | Roles | Description |
|
||||
|-------|------|-------|-------------|
|
||||
| Protected | ✅ | superadmin, admin | Approve/reject/close job |
|
||||
|
||||
### Duplicate Job
|
||||
```http
|
||||
POST /api/v1/jobs/{id}/duplicate
|
||||
```
|
||||
| Field | Auth | Roles | Description |
|
||||
|-------|------|-------|-------------|
|
||||
| Protected | ✅ | superadmin, admin | Duplicate job posting |
|
||||
|
||||
---
|
||||
|
||||
## 📝 Module: Applications
|
||||
|
||||
### Create Application
|
||||
```http
|
||||
POST /api/v1/applications
|
||||
```
|
||||
| Field | Auth | Roles | Description |
|
||||
|-------|------|-------|-------------|
|
||||
| Public | ❌ | Guest/JobSeeker | Apply to a job |
|
||||
|
||||
**Request:**
|
||||
```json
|
||||
{
|
||||
"job_id": 123,
|
||||
"name": "John Doe",
|
||||
"email": "john@example.com",
|
||||
"phone": "+55 11 99999-9999",
|
||||
"message": "I am interested in this position...",
|
||||
"resume_url": "https://..."
|
||||
}
|
||||
# Runs migrations ONLY
|
||||
npm run migrate
|
||||
```
|
||||
|
||||
### List Applications
|
||||
```http
|
||||
GET /api/v1/applications
|
||||
```
|
||||
| Field | Auth | Roles | Description |
|
||||
|-------|------|-------|-------------|
|
||||
| Public | ❌ | admin, recruiter | List applications |
|
||||
|
||||
### Get Application by ID
|
||||
```http
|
||||
GET /api/v1/applications/{id}
|
||||
```
|
||||
| Field | Auth | Roles | Description |
|
||||
|-------|------|-------|-------------|
|
||||
| Public | ❌ | admin, recruiter | Get application details |
|
||||
|
||||
### Update Application Status
|
||||
```http
|
||||
PUT /api/v1/applications/{id}/status
|
||||
```
|
||||
| Field | Auth | Roles | Description |
|
||||
|-------|------|-------|-------------|
|
||||
| Public | ❌ | admin, recruiter | Update application status |
|
||||
|
||||
**Request:**
|
||||
```json
|
||||
{
|
||||
"status": "hired",
|
||||
"notes": "Approved after final interview"
|
||||
}
|
||||
```
|
||||
|
||||
**Status Values:**
|
||||
- `pending` - Awaiting review
|
||||
- `reviewed` - Seen by recruiter
|
||||
- `shortlisted` - Selected for interview
|
||||
- `rejected` - Not selected
|
||||
- `hired` - Offer accepted
|
||||
|
||||
---
|
||||
|
||||
## 🔔 Module: Notifications
|
||||
|
||||
### List Notifications
|
||||
```http
|
||||
GET /api/v1/notifications
|
||||
```
|
||||
| Field | Auth | Roles | Description |
|
||||
|-------|------|-------|-------------|
|
||||
| Protected | ✅ | Any authenticated | Get user notifications |
|
||||
|
||||
---
|
||||
|
||||
## 🏷️ Module: Tags
|
||||
|
||||
### List Tags
|
||||
```http
|
||||
GET /api/v1/tags
|
||||
```
|
||||
| Field | Auth | Roles | Description |
|
||||
|-------|------|-------|-------------|
|
||||
| Protected | ✅ | superadmin, admin | List all tags |
|
||||
|
||||
### Create Tag
|
||||
```http
|
||||
POST /api/v1/tags
|
||||
```
|
||||
| Field | Auth | Roles | Description |
|
||||
|-------|------|-------|-------------|
|
||||
| Protected | ✅ | superadmin, admin | Create new tag |
|
||||
|
||||
### Update Tag
|
||||
```http
|
||||
PATCH /api/v1/tags/{id}
|
||||
```
|
||||
| Field | Auth | Roles | Description |
|
||||
|-------|------|-------|-------------|
|
||||
| Protected | ✅ | superadmin, admin | Update tag |
|
||||
|
||||
---
|
||||
|
||||
## 👤 Module: Candidates
|
||||
|
||||
### List Candidates
|
||||
```http
|
||||
GET /api/v1/candidates
|
||||
```
|
||||
| Field | Auth | Roles | Description |
|
||||
|-------|------|-------|-------------|
|
||||
| Protected | ✅ | superadmin, admin | List all candidates |
|
||||
|
||||
---
|
||||
|
||||
## 📊 Module: Audit
|
||||
|
||||
### List Login Audits
|
||||
```http
|
||||
GET /api/v1/audit/logins
|
||||
```
|
||||
| Field | Auth | Roles | Description |
|
||||
|-------|------|-------|-------------|
|
||||
| Protected | ✅ | superadmin, admin | Login audit trail |
|
||||
|
||||
---
|
||||
|
||||
## 📦 Module: Storage
|
||||
|
||||
### Generate Upload URL
|
||||
```http
|
||||
POST /api/v1/storage/upload-url
|
||||
```
|
||||
| Field | Auth | Roles | Description |
|
||||
|-------|------|-------|-------------|
|
||||
| Protected | ✅ | Any authenticated | Get S3 presigned upload URL |
|
||||
|
||||
### Generate Download URL
|
||||
```http
|
||||
POST /api/v1/storage/download-url
|
||||
```
|
||||
| Field | Auth | Roles | Description |
|
||||
|-------|------|-------|-------------|
|
||||
| Protected | ✅ | Any authenticated | Get S3 presigned download URL |
|
||||
|
||||
### Delete File
|
||||
```http
|
||||
DELETE /api/v1/storage/files
|
||||
```
|
||||
| Field | Auth | Roles | Description |
|
||||
|-------|------|-------|-------------|
|
||||
| Protected | ✅ | Any authenticated | Delete file from S3 |
|
||||
|
||||
---
|
||||
|
||||
## 📚 Module: Documentation
|
||||
|
||||
### Swagger UI
|
||||
```http
|
||||
GET /docs/
|
||||
```
|
||||
| Field | Auth | Roles | Description |
|
||||
|-------|------|-------|-------------|
|
||||
| Public | ❌ | Guest | Interactive API documentation |
|
||||
|
||||
---
|
||||
|
||||
## 🔑 Module: System Credentials
|
||||
|
||||
Manage encrypted credentials for external services (Stripe, SMTP, Storage, etc.).
|
||||
|
||||
### List Configured Services
|
||||
```http
|
||||
GET /api/v1/system/credentials
|
||||
```
|
||||
| Field | Auth | Roles | Description |
|
||||
|-------|------|-------|-------------|
|
||||
| Protected | ✅ | superadmin, admin | List all configured services (metadata only, no secrets) |
|
||||
|
||||
**Response (200):**
|
||||
```json
|
||||
{
|
||||
"services": [
|
||||
{
|
||||
"service_name": "stripe",
|
||||
"updated_at": "2026-02-23T10:30:00Z",
|
||||
"updated_by": "admin-uuid",
|
||||
"is_configured": true
|
||||
},
|
||||
{
|
||||
"service_name": "smtp",
|
||||
"updated_at": "2026-02-22T14:00:00Z",
|
||||
"updated_by": "superadmin-uuid",
|
||||
"is_configured": true
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Save Credentials
|
||||
```http
|
||||
POST /api/v1/system/credentials
|
||||
```
|
||||
| Field | Auth | Roles | Description |
|
||||
|-------|------|-------|-------------|
|
||||
| Protected | ✅ | superadmin, admin | Create or update service credentials |
|
||||
|
||||
**Request:**
|
||||
```json
|
||||
{
|
||||
"serviceName": "stripe",
|
||||
"payload": {
|
||||
"secretKey": "sk_live_xxx",
|
||||
"webhookSecret": "whsec_xxx",
|
||||
"publishableKey": "pk_live_xxx"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Response (200):**
|
||||
```json
|
||||
{
|
||||
"message": "Credentials saved successfully"
|
||||
}
|
||||
```
|
||||
|
||||
**Supported Services:**
|
||||
| Service Name | Fields |
|
||||
|--------------|--------|
|
||||
| `stripe` | `secretKey`, `webhookSecret`, `publishableKey` |
|
||||
| `smtp` | `host`, `port`, `username`, `password`, `from_email`, `from_name`, `secure` |
|
||||
| `storage` | `endpoint`, `region`, `bucket`, `accessKey`, `secretKey` |
|
||||
| `cloudflare_config` | `apiToken`, `zoneId` |
|
||||
| `firebase` | `serviceAccountJson` (JSON string) |
|
||||
| `appwrite` | `endpoint`, `projectId`, `apiKey` |
|
||||
| `lavinmq` | `amqpUrl` |
|
||||
| `cpanel` | `host`, `username`, `apiToken` |
|
||||
|
||||
### Delete Credentials
|
||||
```http
|
||||
DELETE /api/v1/system/credentials/{serviceName}
|
||||
```
|
||||
| Field | Auth | Roles | Description |
|
||||
|-------|------|-------|-------------|
|
||||
| Protected | ✅ | superadmin, admin | Delete service credentials |
|
||||
|
||||
**Response (200):**
|
||||
```json
|
||||
{
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
**Security Notes:**
|
||||
- Credentials are encrypted with RSA-OAEP-SHA256 before storage
|
||||
- Only metadata (service name, timestamps) is returned on list operations
|
||||
- Actual secret values cannot be retrieved after saving
|
||||
- Updating credentials overwrites the existing configuration
|
||||
|
||||
---
|
||||
|
||||
## 🔑 Permission Matrix
|
||||
|
||||
| Route | Guest | JobSeeker | Recruiter | CompanyAdmin | Admin | SuperAdmin |
|
||||
|-------|-------|-----------|-----------|--------------|-------|------------|
|
||||
| `POST /auth/login` | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||
| `POST /auth/register` | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ |
|
||||
| `GET /jobs` | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||
| `GET /jobs/{id}` | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||
| `POST /jobs` | ❌ | ❌ | ✅ | ✅ | ✅ | ✅ |
|
||||
| `POST /applications` | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ |
|
||||
| `GET /users/me` | ❌ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||
| `GET /users` | ❌ | ❌ | ❌ | ❌ | ✅ | ✅ |
|
||||
| `DELETE /users/{id}` | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
|
||||
| `GET /notifications` | ❌ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||
| `GET /audit/logins` | ❌ | ❌ | ❌ | ❌ | ✅ | ✅ |
|
||||
| `GET /jobs/moderation` | ❌ | ❌ | ❌ | ❌ | ✅ | ✅ |
|
||||
|
||||
---
|
||||
|
||||
## 🆔 ID Formats
|
||||
|
||||
| Entity | ID Type | Example |
|
||||
|--------|---------|---------|
|
||||
| Users | INT (SERIAL) | `1`, `42`, `1337` |
|
||||
| Companies | INT (SERIAL) | `1`, `15`, `100` |
|
||||
| Jobs | INT (SERIAL) | `1`, `500`, `2500` |
|
||||
| Notifications | UUID v7 | `019438a1-2b3c-7abc-8123-4567890abcdef` |
|
||||
| Tickets | UUID v7 | `019438a2-3c4d-7xyz-9abc-def0123456789` |
|
||||
| Payments | UUID v7 | `019438a3-4e5f-7mno-pqrs-tuvwxyz012345` |
|
||||
|
||||
---
|
||||
|
||||
## 📝 Error Responses
|
||||
|
||||
### 400 Bad Request
|
||||
```json
|
||||
{
|
||||
"error": "Invalid request body",
|
||||
"details": "Field 'email' is required"
|
||||
}
|
||||
```
|
||||
|
||||
### 401 Unauthorized
|
||||
```json
|
||||
{
|
||||
"error": "Unauthorized",
|
||||
"message": "Invalid or expired token"
|
||||
}
|
||||
```
|
||||
|
||||
### 403 Forbidden
|
||||
```json
|
||||
{
|
||||
"error": "Forbidden",
|
||||
"message": "Insufficient permissions"
|
||||
}
|
||||
```
|
||||
|
||||
### 404 Not Found
|
||||
```json
|
||||
{
|
||||
"error": "Not Found",
|
||||
"message": "Resource not found"
|
||||
}
|
||||
```
|
||||
|
||||
### 500 Internal Server Error
|
||||
```json
|
||||
{
|
||||
"error": "Internal Server Error",
|
||||
"message": "An unexpected error occurred"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📚 Related Documentation
|
||||
|
||||
- [Database Schema](DATABASE.md)
|
||||
- [Roadmap](ROADMAP.md)
|
||||
- [Tasks](TASKS.md)
|
||||
The seeder automatically inserts the `Admin@2025!` superadmin hash required for local testing.
|
||||
|
|
|
|||
794
docs/DATABASE.md
794
docs/DATABASE.md
|
|
@ -1,751 +1,131 @@
|
|||
# 🗄️ Database Schema Documentation
|
||||
# 🗄️ Database Architecture - GoHorseJobs
|
||||
|
||||
Complete database documentation for the GoHorseJobs platform.
|
||||
|
||||
> **Last Updated:** 2026-02-16
|
||||
> **Database:** PostgreSQL 16+ (Local `postgres-main` container)
|
||||
> **Connection:** Internal `gohorsejobs_dev` database via `web_proxy` network
|
||||
> **ID Strategy:** UUID v7 for core tables, SERIAL for reference tables
|
||||
> **Migrations:** 30 SQL files in `backend/migrations/`
|
||||
GoHorseJobs uses a single **PostgreSQL 16+** database shared across all services (Backend API, NestJS Backoffice, Node.js Seeder).
|
||||
|
||||
---
|
||||
|
||||
## <EFBFBD>️ Development Environment Structure
|
||||
## 🏗️ Core Entity-Relationship Diagram (ERD)
|
||||
|
||||
The development environment (`apolo` server) uses a **Local Containerized Strategy** to ensure isolation and speed.
|
||||
|
||||
### 🏗️ Topology
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
subgraph VPS ["Apolo Server (VPS)"]
|
||||
subgraph Net ["Docker Network: web_proxy"]
|
||||
PG[("postgres-main")]
|
||||
BE["Backend API"]
|
||||
BO["Backoffice"]
|
||||
SE["Seeder API"]
|
||||
end
|
||||
Traefik["Traefik Proxy"]
|
||||
end
|
||||
|
||||
Traefik --> BE
|
||||
Traefik --> BO
|
||||
Traefik --> SE
|
||||
|
||||
BE -- "internal:5432" --> PG
|
||||
BO -- "internal:5432" --> PG
|
||||
SE -- "internal:5432" --> PG
|
||||
|
||||
style PG fill:#336791,stroke:#fff,stroke-width:2px,color:#fff
|
||||
```
|
||||
|
||||
### 🔌 Connection Details
|
||||
|
||||
All services connect to the database via the internal Docker network.
|
||||
|
||||
| Parameter | Value | Notes |
|
||||
|-----------|-------|-------|
|
||||
| **Host** | `postgres-main` | Internal Container Hostname |
|
||||
| **Port** | `5432` | Internal Port |
|
||||
| **Database** | `gohorsejobs_dev` | Dedicated Dev DB (Isolated from `main_db`) |
|
||||
| **User** | `yuki` | Owner of public schema |
|
||||
| **Network** | `web_proxy` | Shared Bridge Network |
|
||||
| **SSL Mode** | `disable` | Internal traffic is unencrypted |
|
||||
|
||||
### 🚀 Access & Management
|
||||
|
||||
Since the database runs inside a container and is not exposed to the public internet, use the following methods for access:
|
||||
|
||||
**1. CLI Access (via SSH)**
|
||||
```bash
|
||||
# Connect to PostgreSQL shell
|
||||
ssh root@apolo 'podman exec -it postgres-main psql -U yuki -d gohorsejobs_dev'
|
||||
```
|
||||
|
||||
**2. Run Migrations**
|
||||
Migrations are applied using the Backend service or manually piped:
|
||||
```bash
|
||||
# Manual Pipe (from local machine)
|
||||
cat backend/migrations/*.sql | ssh root@apolo 'podman exec -i postgres-main psql -U yuki -d gohorsejobs_dev'
|
||||
```
|
||||
|
||||
**3. Seeding Data**
|
||||
Trigger the Seeder API (running locally) to populate data:
|
||||
```bash
|
||||
curl -X POST https://seeder.gohorsejobs.com/seed
|
||||
```
|
||||
|
||||
|
||||
## <20>📊 Entity Relationship Diagram
|
||||
The core data model centers around `users` acting in different capacities (`candidate`, `recruiter`, `admin`) interacting with `companies` and `jobs`.
|
||||
|
||||
```mermaid
|
||||
erDiagram
|
||||
%% Core Entities
|
||||
users ||--o{ user_companies : "belongs to"
|
||||
users ||--o{ user_roles : "has roles"
|
||||
users ||--o{ applications : "submits"
|
||||
users ||--o{ favorite_jobs : "saves"
|
||||
users ||--o{ notifications : "receives"
|
||||
users ||--o{ tickets : "opens"
|
||||
users ||--o{ ticket_messages : "sends"
|
||||
users ||--o{ login_audits : "generates"
|
||||
users ||--o{ activity_logs : "generates"
|
||||
USERS ||--o{ USER_COMPANIES : "belongs to"
|
||||
COMPANIES ||--o{ USER_COMPANIES : "contains"
|
||||
|
||||
companies ||--o{ user_companies : "has members"
|
||||
companies ||--o{ jobs : "posts"
|
||||
COMPANIES ||--o{ JOBS : "posts"
|
||||
USERS ||--o{ JOBS : "creates (author)"
|
||||
|
||||
jobs ||--o{ applications : "receives"
|
||||
jobs ||--o{ favorite_jobs : "saved by"
|
||||
jobs ||--o{ job_payments : "has payments"
|
||||
USERS ||--o{ APPLICATIONS : "submits"
|
||||
JOBS ||--o{ APPLICATIONS : "receives"
|
||||
|
||||
regions ||--o{ cities : "contains"
|
||||
regions ||--o{ companies : "located in"
|
||||
regions ||--o{ jobs : "located in"
|
||||
USERS ||--o{ FAVORITE_JOBS : "saves"
|
||||
JOBS ||--o{ FAVORITE_JOBS : "favorited"
|
||||
|
||||
tickets ||--o{ ticket_messages : "contains"
|
||||
USERS ||--o{ TICKETS : "opens"
|
||||
USERS ||--o{ TICKET_MESSAGES : "sends"
|
||||
TICKETS ||--o{ TICKET_MESSAGES : "has"
|
||||
|
||||
%% Entities
|
||||
users {
|
||||
int id PK "SERIAL"
|
||||
varchar identifier UK "login"
|
||||
varchar password_hash
|
||||
varchar role "enum"
|
||||
varchar full_name
|
||||
varchar email
|
||||
varchar name
|
||||
int tenant_id FK "nullable"
|
||||
varchar status
|
||||
USERS {
|
||||
uuid id PK
|
||||
string role "superadmin, admin, recruiter, candidate"
|
||||
string identifier "username or email"
|
||||
string email UK
|
||||
string password_hash
|
||||
boolean is_active
|
||||
timestamp created_at
|
||||
}
|
||||
|
||||
user_roles {
|
||||
int user_id PK,FK
|
||||
varchar role PK "composite key"
|
||||
COMPANIES {
|
||||
uuid id PK
|
||||
string name
|
||||
string document "CNPJ"
|
||||
string domain UK
|
||||
string location
|
||||
string industry
|
||||
string logo_url
|
||||
string status "pending, active, rejected"
|
||||
}
|
||||
|
||||
companies {
|
||||
int id PK "SERIAL"
|
||||
varchar name
|
||||
varchar slug UK
|
||||
varchar type
|
||||
varchar document
|
||||
text address
|
||||
int region_id FK
|
||||
varchar email
|
||||
varchar website
|
||||
boolean verified
|
||||
boolean active
|
||||
USER_COMPANIES {
|
||||
uuid id PK
|
||||
uuid user_id FK
|
||||
uuid company_id FK
|
||||
string role "owner, admin, member"
|
||||
}
|
||||
|
||||
jobs {
|
||||
int id PK "SERIAL"
|
||||
int company_id FK
|
||||
int created_by FK
|
||||
varchar title
|
||||
JOBS {
|
||||
uuid id PK
|
||||
uuid company_id FK
|
||||
uuid author_id FK
|
||||
string title
|
||||
text description
|
||||
decimal salary_min
|
||||
decimal salary_max
|
||||
varchar salary_type
|
||||
varchar currency
|
||||
varchar employment_type
|
||||
varchar work_mode
|
||||
varchar status
|
||||
boolean is_featured
|
||||
string type "full-time, remote, etc"
|
||||
decimal max_salary
|
||||
decimal min_salary
|
||||
string status "draft, published, closed"
|
||||
}
|
||||
|
||||
applications {
|
||||
int id PK "SERIAL"
|
||||
int job_id FK
|
||||
int user_id FK "nullable"
|
||||
varchar status
|
||||
text message
|
||||
varchar resume_url
|
||||
}
|
||||
|
||||
regions {
|
||||
int id PK "SERIAL"
|
||||
varchar name
|
||||
varchar country_code
|
||||
varchar code
|
||||
}
|
||||
|
||||
cities {
|
||||
int id PK "SERIAL"
|
||||
int region_id FK
|
||||
varchar name
|
||||
}
|
||||
|
||||
notifications {
|
||||
APPLICATIONS {
|
||||
uuid id PK
|
||||
int user_id FK
|
||||
varchar type
|
||||
varchar title
|
||||
text message
|
||||
boolean read
|
||||
uuid job_id FK
|
||||
uuid candidate_id FK
|
||||
string status "pending, reviewing, interviewed, rejected, accepted"
|
||||
text cover_letter
|
||||
}
|
||||
|
||||
job_payments {
|
||||
FAVORITE_JOBS {
|
||||
uuid id PK
|
||||
int job_id FK
|
||||
int user_id FK
|
||||
decimal amount
|
||||
varchar status
|
||||
varchar stripe_session_id
|
||||
uuid user_id FK
|
||||
uuid job_id FK
|
||||
timestamp created_at
|
||||
}
|
||||
|
||||
tickets {
|
||||
TICKETS {
|
||||
uuid id PK
|
||||
int user_id FK
|
||||
varchar subject
|
||||
varchar status
|
||||
varchar priority
|
||||
}
|
||||
|
||||
ticket_messages {
|
||||
uuid id PK
|
||||
uuid ticket_id FK
|
||||
int user_id FK
|
||||
text message
|
||||
boolean is_staff
|
||||
}
|
||||
|
||||
job_posting_prices {
|
||||
int id PK
|
||||
varchar name
|
||||
decimal price
|
||||
int duration_days
|
||||
}
|
||||
|
||||
login_audits {
|
||||
int id PK
|
||||
int user_id FK
|
||||
varchar identifier
|
||||
boolean success
|
||||
varchar ip_address
|
||||
}
|
||||
|
||||
activity_logs {
|
||||
int id PK
|
||||
int user_id FK
|
||||
varchar entity_type
|
||||
varchar action
|
||||
jsonb details
|
||||
uuid user_id FK "nullable for guests"
|
||||
string subject
|
||||
string category "Support, Bug, Billing..."
|
||||
string status "open, in_progress, resolved, closed"
|
||||
string email "fallback for guests"
|
||||
string name "fallback for guests"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🏗️ Table Overview
|
||||
## 🔑 Key Strategies
|
||||
|
||||
| Table | ID Type | Description | Migration |
|
||||
|-------|---------|-------------|-----------|
|
||||
| `users` | UUID v7 | System users (candidates, recruiters, admins) | 001/021 |
|
||||
| `user_roles` | Composite | Additional roles per user | 020 |
|
||||
| `companies` | UUID v7 | Employer organizations | 002/021 |
|
||||
| `user_companies` | UUID v7 | User ↔ Company mapping | 003/021 |
|
||||
| `regions` | SERIAL | States/Provinces/Prefectures | 004 |
|
||||
| `cities` | SERIAL | Cities within regions | 004 |
|
||||
| `jobs` | UUID v7 | Job postings | 005/021 |
|
||||
| `applications` | UUID v7 | Job applications | 006/021 |
|
||||
| `favorite_jobs` | SERIAL | Saved jobs by users | 007 |
|
||||
| `password_resets` | SERIAL | Password reset tokens | 008 |
|
||||
| `notifications` | UUID v7 | User notifications | 016 |
|
||||
| `tickets` | UUID v7 | Support tickets | 017 |
|
||||
| `ticket_messages` | UUID v7 | Ticket messages | 017 |
|
||||
| `job_payments` | UUID v7 | Payment records | 019 |
|
||||
| `job_posting_prices` | SERIAL | Pricing configuration | 019 |
|
||||
| `login_audits` | SERIAL | Login audit trail | 013 |
|
||||
| `activity_logs` | SERIAL | Activity logging | 013 |
|
||||
### 1. Primary Keys (UUID v7)
|
||||
We aggressively use **UUID v7** for all major business entities (`users`, `companies`, `jobs`).
|
||||
* **Why?** UUID v7 contains a timestamp component, making it naturally sortable by insertion time (like SERIAL/auto-increment) while avoiding the predictability and distributed generation bottlenecks of standard integers.
|
||||
|
||||
---
|
||||
### 2. Multi-Role Profiles
|
||||
The system handles permissions through the `role` enum in the `users` table:
|
||||
* `superadmin`: Global control over GoHorseJobs.
|
||||
* `admin`: Can moderate companies/jobs inside the Backoffice.
|
||||
* `recruiter`: A user attached to a `company_id` via `user_companies` who can post `jobs`.
|
||||
* `candidate`: A standard user seeking jobs and making `applications`.
|
||||
|
||||
## 📋 Core Tables
|
||||
A single email can only correspond to one role globally. If a user needs another role, they must change it or use a different credential.
|
||||
|
||||
### `users`
|
||||
### 3. Migrations
|
||||
Migrations are written in pure SQL and stored in `backend/migrations/`.
|
||||
They follow a strict sequential numbering index (e.g., `000_schema.sql`, `001_roles.sql`, etc). The backend runtime executes these on startup if the database is out of sync.
|
||||
|
||||
System users including Candidates, Recruiters, and Admins.
|
||||
|
||||
```sql
|
||||
CREATE TABLE users (
|
||||
id SERIAL PRIMARY KEY,
|
||||
identifier VARCHAR(100) UNIQUE NOT NULL, -- Login (username/email)
|
||||
password_hash VARCHAR(255) NOT NULL,
|
||||
role VARCHAR(20) NOT NULL, -- 'superadmin'|'admin'|'recruiter'|'candidate'
|
||||
|
||||
-- Profile
|
||||
full_name VARCHAR(255),
|
||||
email VARCHAR(255),
|
||||
name VARCHAR(255),
|
||||
phone VARCHAR(30),
|
||||
|
||||
-- Candidate Profile Fields (015)
|
||||
city VARCHAR(100),
|
||||
state VARCHAR(50),
|
||||
title VARCHAR(100),
|
||||
experience VARCHAR(100),
|
||||
skills TEXT[],
|
||||
bio TEXT,
|
||||
objective TEXT,
|
||||
|
||||
-- Multi-tenancy (020)
|
||||
tenant_id INT REFERENCES companies(id),
|
||||
status VARCHAR(20) DEFAULT 'active',
|
||||
|
||||
-- Settings
|
||||
language VARCHAR(5) DEFAULT 'en',
|
||||
active BOOLEAN DEFAULT true,
|
||||
|
||||
-- Timestamps
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
last_login_at TIMESTAMP
|
||||
);
|
||||
```
|
||||
|
||||
**Roles:**
|
||||
- `superadmin` - Platform administrator
|
||||
- `admin` - Company administrator
|
||||
- `recruiter` - Job poster/recruiter
|
||||
- `candidate` - Candidate/job seeker
|
||||
|
||||
---
|
||||
|
||||
### `user_roles`
|
||||
|
||||
Additional roles per user (for multi-role support).
|
||||
|
||||
```sql
|
||||
CREATE TABLE user_roles (
|
||||
user_id INT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
role VARCHAR(50) NOT NULL,
|
||||
PRIMARY KEY (user_id, role)
|
||||
);
|
||||
### 4. Background Seeding
|
||||
Seeding (populating test data) is explicitly decoupled from migrations. It lives isolated in the Node.js `seeder-api`.
|
||||
To reset your local database to a clean, populated state:
|
||||
```bash
|
||||
# Deletes everything, remigrates, and inserts base data (except 153k cities)
|
||||
cd seeder-api && npm run seed:lite
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `companies`
|
||||
## ⚠️ Security Notes
|
||||
|
||||
Employer organizations that post jobs.
|
||||
|
||||
```sql
|
||||
CREATE TABLE companies (
|
||||
id SERIAL PRIMARY KEY,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
slug VARCHAR(255) UNIQUE NOT NULL, -- URL-friendly name
|
||||
type VARCHAR(50) DEFAULT 'company', -- 'company'|'agency'|'system'
|
||||
document VARCHAR(100), -- CNPJ/Tax ID
|
||||
|
||||
-- Location
|
||||
address TEXT,
|
||||
region_id INT REFERENCES regions(id),
|
||||
city_id INT REFERENCES cities(id),
|
||||
|
||||
-- Contact
|
||||
phone VARCHAR(30),
|
||||
email VARCHAR(255),
|
||||
website VARCHAR(255),
|
||||
|
||||
-- Branding
|
||||
logo_url TEXT,
|
||||
description TEXT, -- JSON or plain text
|
||||
|
||||
-- Status
|
||||
active BOOLEAN DEFAULT true,
|
||||
verified BOOLEAN DEFAULT false,
|
||||
|
||||
-- Timestamps
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `jobs`
|
||||
|
||||
Job postings created by companies.
|
||||
|
||||
```sql
|
||||
CREATE TABLE jobs (
|
||||
id SERIAL PRIMARY KEY,
|
||||
company_id INT NOT NULL REFERENCES companies(id) ON DELETE CASCADE,
|
||||
created_by INT NOT NULL REFERENCES users(id),
|
||||
|
||||
-- Job Details
|
||||
title VARCHAR(255) NOT NULL,
|
||||
description TEXT NOT NULL,
|
||||
|
||||
-- Salary
|
||||
salary_min DECIMAL(12,2),
|
||||
salary_max DECIMAL(12,2),
|
||||
salary_type VARCHAR(20), -- 'hourly'|'daily'|'weekly'|'monthly'|'yearly'
|
||||
currency VARCHAR(3), -- 'BRL'|'USD'|'EUR'|'GBP'|'JPY'
|
||||
|
||||
-- Employment
|
||||
employment_type VARCHAR(30), -- 'full-time'|'part-time'|'contract'|'dispatch'|...
|
||||
work_mode VARCHAR(10), -- 'onsite'|'hybrid'|'remote'
|
||||
working_hours VARCHAR(100),
|
||||
|
||||
-- Location
|
||||
location VARCHAR(255),
|
||||
region_id INT REFERENCES regions(id),
|
||||
city_id INT REFERENCES cities(id),
|
||||
|
||||
-- Requirements & Benefits
|
||||
requirements JSONB, -- Array of skills/requirements
|
||||
benefits JSONB, -- Array of benefits
|
||||
|
||||
-- Visa & Language
|
||||
visa_support BOOLEAN DEFAULT false,
|
||||
language_level VARCHAR(20), -- 'N5'|'N4'|'N3'|'N2'|'N1'|'beginner'|'none'
|
||||
|
||||
-- Status
|
||||
status VARCHAR(20) DEFAULT 'open', -- 'draft'|'open'|'closed'|'published'|...
|
||||
is_featured BOOLEAN DEFAULT false,
|
||||
|
||||
-- Timestamps
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
```
|
||||
|
||||
**Status Values:**
|
||||
- `draft` - Not published yet
|
||||
- `open` - Accepting applications
|
||||
- `published` - Live and visible
|
||||
- `paused` - Temporarily hidden
|
||||
- `closed` - No longer accepting
|
||||
- `expired` - Past expiration date
|
||||
- `archived` - Archived by employer
|
||||
- `reported` - Flagged for review
|
||||
- `review` - Under admin review
|
||||
|
||||
---
|
||||
|
||||
### `applications`
|
||||
|
||||
Job applications from candidates.
|
||||
|
||||
```sql
|
||||
CREATE TABLE applications (
|
||||
id SERIAL PRIMARY KEY,
|
||||
job_id INT NOT NULL REFERENCES jobs(id) ON DELETE CASCADE,
|
||||
user_id INT REFERENCES users(id), -- Nullable for guest applications
|
||||
|
||||
-- Applicant Info
|
||||
name VARCHAR(255) NOT NULL,
|
||||
email VARCHAR(255) NOT NULL,
|
||||
phone VARCHAR(30),
|
||||
line_id VARCHAR(100),
|
||||
whatsapp VARCHAR(30),
|
||||
|
||||
-- Application Content
|
||||
message TEXT,
|
||||
resume_url VARCHAR(500),
|
||||
documents JSONB,
|
||||
|
||||
-- Status
|
||||
status VARCHAR(20) DEFAULT 'pending', -- 'pending'|'reviewed'|'shortlisted'|'rejected'|'hired'
|
||||
notes TEXT, -- Recruiter notes
|
||||
|
||||
-- Timestamps
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🌍 Geographic Tables
|
||||
|
||||
### `regions`
|
||||
|
||||
States, provinces, or prefectures.
|
||||
|
||||
```sql
|
||||
CREATE TABLE regions (
|
||||
id SERIAL PRIMARY KEY,
|
||||
name VARCHAR(100) NOT NULL,
|
||||
country_code VARCHAR(2) NOT NULL, -- 'BR'|'US'|'JP'
|
||||
code VARCHAR(10) NOT NULL -- 'SP'|'CA'|'13'
|
||||
);
|
||||
```
|
||||
|
||||
**Seeded Regions:**
|
||||
- 🇧🇷 Brazil: São Paulo (SP), Rio de Janeiro (RJ), Minas Gerais (MG)
|
||||
- 🇺🇸 USA: California (CA), New York (NY), Texas (TX)
|
||||
- 🇯🇵 Japan: Tokyo (13), Osaka (27)
|
||||
|
||||
---
|
||||
|
||||
### `cities`
|
||||
|
||||
Cities within regions.
|
||||
|
||||
```sql
|
||||
CREATE TABLE cities (
|
||||
id SERIAL PRIMARY KEY,
|
||||
region_id INT NOT NULL REFERENCES regions(id),
|
||||
name VARCHAR(100) NOT NULL
|
||||
);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 💰 Payment Tables
|
||||
|
||||
### `job_payments` (UUID)
|
||||
|
||||
Payment records for job postings.
|
||||
|
||||
```sql
|
||||
CREATE TABLE job_payments (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
job_id INT NOT NULL REFERENCES jobs(id) ON DELETE CASCADE,
|
||||
user_id INT REFERENCES users(id) ON DELETE SET NULL,
|
||||
|
||||
-- Stripe
|
||||
stripe_session_id VARCHAR(255),
|
||||
stripe_payment_intent VARCHAR(255),
|
||||
stripe_customer_id VARCHAR(255),
|
||||
|
||||
-- Amount
|
||||
amount DECIMAL(12,2) NOT NULL,
|
||||
currency VARCHAR(3) DEFAULT 'USD',
|
||||
|
||||
-- Status
|
||||
status VARCHAR(20) DEFAULT 'pending', -- 'pending'|'completed'|'failed'|'refunded'|'expired'
|
||||
|
||||
-- Billing
|
||||
billing_type VARCHAR(20), -- 'company'|'individual'
|
||||
billing_name VARCHAR(255),
|
||||
billing_email VARCHAR(255),
|
||||
|
||||
-- Job Details
|
||||
duration_days INT DEFAULT 30,
|
||||
is_featured BOOLEAN DEFAULT false,
|
||||
|
||||
-- Timestamps
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
paid_at TIMESTAMP,
|
||||
expires_at TIMESTAMP
|
||||
);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `job_posting_prices`
|
||||
|
||||
Pricing configuration for job postings.
|
||||
|
||||
```sql
|
||||
CREATE TABLE job_posting_prices (
|
||||
id SERIAL PRIMARY KEY,
|
||||
name VARCHAR(100) NOT NULL,
|
||||
description TEXT,
|
||||
price DECIMAL(12,2) NOT NULL,
|
||||
currency VARCHAR(3) DEFAULT 'USD',
|
||||
duration_days INT DEFAULT 30,
|
||||
is_featured BOOLEAN DEFAULT false,
|
||||
stripe_price_id VARCHAR(255),
|
||||
active BOOLEAN DEFAULT true,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔔 Notification Tables
|
||||
|
||||
### `notifications` (UUID)
|
||||
|
||||
User notifications.
|
||||
|
||||
```sql
|
||||
CREATE TABLE notifications (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_id INT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
type VARCHAR(50),
|
||||
title VARCHAR(255),
|
||||
message TEXT,
|
||||
read BOOLEAN DEFAULT false,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎫 Support Tables
|
||||
|
||||
### `tickets` (UUID)
|
||||
|
||||
Support tickets.
|
||||
|
||||
```sql
|
||||
CREATE TABLE tickets (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_id INT NOT NULL,
|
||||
subject VARCHAR(255) NOT NULL,
|
||||
status VARCHAR(20) DEFAULT 'open', -- 'open'|'in_progress'|'resolved'|'closed'
|
||||
priority VARCHAR(10) DEFAULT 'medium', -- 'low'|'medium'|'high'|'urgent'
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
```
|
||||
|
||||
### `ticket_messages` (UUID)
|
||||
|
||||
Messages within tickets.
|
||||
|
||||
```sql
|
||||
CREATE TABLE ticket_messages (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
ticket_id UUID NOT NULL REFERENCES tickets(id) ON DELETE CASCADE,
|
||||
user_id INT NOT NULL,
|
||||
message TEXT NOT NULL,
|
||||
is_staff BOOLEAN DEFAULT false,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📝 Audit Tables
|
||||
|
||||
### `login_audits`
|
||||
|
||||
Login attempt tracking.
|
||||
|
||||
```sql
|
||||
CREATE TABLE login_audits (
|
||||
id SERIAL PRIMARY KEY,
|
||||
user_id INT,
|
||||
identifier VARCHAR(100),
|
||||
success BOOLEAN,
|
||||
ip_address VARCHAR(45),
|
||||
user_agent TEXT,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
```
|
||||
|
||||
### `activity_logs`
|
||||
|
||||
General activity logging.
|
||||
|
||||
```sql
|
||||
CREATE TABLE activity_logs (
|
||||
id SERIAL PRIMARY KEY,
|
||||
user_id INT,
|
||||
entity_type VARCHAR(50),
|
||||
entity_id INT,
|
||||
action VARCHAR(50),
|
||||
details JSONB,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔗 Junction Tables
|
||||
|
||||
### `user_companies`
|
||||
|
||||
Maps users to companies (N:M relationship).
|
||||
|
||||
```sql
|
||||
CREATE TABLE user_companies (
|
||||
id SERIAL PRIMARY KEY,
|
||||
user_id INT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
company_id INT NOT NULL REFERENCES companies(id) ON DELETE CASCADE,
|
||||
role VARCHAR(30) DEFAULT 'recruiter', -- 'admin'|'recruiter'
|
||||
permissions JSONB,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
UNIQUE(user_id, company_id)
|
||||
);
|
||||
```
|
||||
|
||||
### `favorite_jobs`
|
||||
|
||||
Saved jobs by users.
|
||||
|
||||
```sql
|
||||
CREATE TABLE favorite_jobs (
|
||||
id SERIAL PRIMARY KEY,
|
||||
user_id INT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
job_id INT NOT NULL REFERENCES jobs(id) ON DELETE CASCADE,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
UNIQUE(user_id, job_id)
|
||||
);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔄 Migrations History
|
||||
|
||||
| # | File | Description |
|
||||
|---|------|-------------|
|
||||
| 001 | `001_create_users_table.sql` | Core users table |
|
||||
| 002 | `002_create_companies_table.sql` | Companies/employers |
|
||||
| 003 | `003_create_user_companies_table.sql` | User ↔ Company mapping |
|
||||
| 004 | `004_create_prefectures_cities_tables.sql` | Geographic data |
|
||||
| 005 | `005_create_jobs_table.sql` | Job postings |
|
||||
| 006 | `006_create_applications_table.sql` | Job applications |
|
||||
| 007 | `007_create_favorite_jobs_table.sql` | Saved jobs |
|
||||
| 008 | `008_create_password_resets_table.sql` | Password reset tokens |
|
||||
| 010 | `010_seed_super_admin.sql` | Default superadmin |
|
||||
| 011 | `011_add_is_featured_to_jobs.sql` | Featured jobs flag |
|
||||
| 012 | `012_add_work_mode.sql` | Work mode column |
|
||||
| 013 | `013_create_backoffice_tables.sql` | Audit tables |
|
||||
| 014 | `014_update_job_status_constraint.sql` | Status enum update |
|
||||
| 015 | `015_add_candidate_profile_fields.sql` | Candidate profile |
|
||||
| 016 | `016_create_notifications_table.sql` | Notifications |
|
||||
| 017 | `017_create_tickets_table.sql` | Support tickets |
|
||||
| 018 | `018_add_currency_to_jobs.sql` | Currency support |
|
||||
| 019 | `019_create_job_payments_table.sql` | Payment tracking |
|
||||
| 020 | `020_unify_schema.sql` | Schema unification |
|
||||
| 999 | `999_fix_gohorse_schema.sql` | Schema fixes |
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ ID Strategy Notes
|
||||
|
||||
The database uses a **hybrid ID strategy**:
|
||||
|
||||
| Strategy | Tables | Rationale |
|
||||
|----------|--------|-----------|
|
||||
| **UUID v7** | users, companies, jobs, applications, notifications, tickets, job_payments | Time-ordered, distributed-friendly, sortable, scalable |
|
||||
| **SERIAL (INT)** | regions, cities, job_posting_prices, job_tags | Reference/Static data, low volume |
|
||||
|
||||
### UUID v7 (RFC 9562)
|
||||
|
||||
Starting from migration `021_create_uuid_v7_function.sql`, the database uses **UUID v7** instead of UUID v4:
|
||||
|
||||
```sql
|
||||
-- UUID v7 is generated by uuid_generate_v7() function
|
||||
SELECT uuid_generate_v7();
|
||||
-- Returns: 019438a1-2b3c-7abc-8123-4567890abcdef
|
||||
-- ^^^^^^^^ time component (sortable)
|
||||
```
|
||||
|
||||
**Benefits of UUID v7:**
|
||||
- ⏱️ Time-ordered (sortable by creation time)
|
||||
- 🌐 Distributed-friendly (no coordination needed)
|
||||
- 📊 Better index performance than UUID v4
|
||||
- 🔒 Contains embedded timestamp
|
||||
|
||||
---
|
||||
|
||||
## 🔒 Security Notes
|
||||
|
||||
1. **Password Hashing:** BCrypt with optional `PASSWORD_PEPPER` environment variable
|
||||
2. **Soft Deletes:** Not implemented (uses `CASCADE` for referential integrity)
|
||||
3. **Row-Level Security:** Not implemented (uses application-level checks)
|
||||
4. **Audit Trail:** `login_audits` and `activity_logs` for compliance
|
||||
|
||||
---
|
||||
|
||||
## 📚 Related Documentation
|
||||
|
||||
- [Backend README](../backend/README.md) - API documentation
|
||||
- [Seeder README](../seeder-api/README.md) - Database seeding
|
||||
- [Backoffice README](../backoffice/README.md) - Admin panel
|
||||
### The `password_hash` & Pepper
|
||||
Bcrypt hashes stored in the standard `users` table require a secret pepper (`PASSWORD_PEPPER`).
|
||||
* The raw hash stored in the DB cannot be cracked immediately due to the pepper.
|
||||
* The seeder relies on reading the `PASSWORD_PEPPER` from `.env` to forge the initial super-admin and test accounts hashes. **If you change the `.env` pepper, the entire DB of seeded passwords immediately invalidates.** You must re-run `npm run seed`.
|
||||
|
|
|
|||
706
docs/DEVOPS.md
706
docs/DEVOPS.md
|
|
@ -1,651 +1,119 @@
|
|||
# DevOps - GoHorseJobs (Development Environment)
|
||||
# ☁️ DevOps & Infrastructure - GoHorseJobs
|
||||
|
||||
Infraestrutura, CI/CD e deploy do projeto GoHorseJobs no servidor `apolo`.
|
||||
|
||||
> **Last Updated:** 2026-02-18
|
||||
> **Servers:** Apolo VPS (Podman), Redbull VPS (Coolify)
|
||||
> **Tech Stack:** Podman, Systemd (Quadlet), Traefik, PostgreSQL, Coolify
|
||||
|
||||
---
|
||||
|
||||
## ☁️ Cloudflare DNS Zone
|
||||
|
||||
### Zone Info
|
||||
|
||||
| Property | Value |
|
||||
|----------|-------|
|
||||
| **Zone ID** | `5e7e9286849525abf7f30b451b7964ac` |
|
||||
| **Domain** | gohorsejobs.com |
|
||||
| **Account** | gohorsejobs |
|
||||
| **Email** | yamamoto@rede5.com.br |
|
||||
| **Plan** | Free Website |
|
||||
| **Name Servers** | chuck.ns.cloudflare.com, fatima.ns.cloudflare.com |
|
||||
|
||||
### API Access
|
||||
|
||||
```bash
|
||||
# Token location: ~/.ssh/cloudflare-token
|
||||
export CF_AUTH_EMAIL="yamamoto@rede5.com.br"
|
||||
export CF_AUTH_KEY="5dcfd89a9d4ec330dede0d4074a518f26818e"
|
||||
|
||||
# List zones
|
||||
curl -s -H "X-Auth-Email: $CF_AUTH_EMAIL" -H "X-Auth-Key: $CF_AUTH_KEY" \
|
||||
"https://api.cloudflare.com/client/v4/zones"
|
||||
|
||||
# List DNS records
|
||||
curl -s -H "X-Auth-Email: $CF_AUTH_EMAIL" -H "X-Auth-Key: $CF_AUTH_KEY" \
|
||||
"https://api.cloudflare.com/client/v4/zones/5e7e9286849525abf7f30b451b7964ac/dns_records"
|
||||
|
||||
# Purge cache
|
||||
curl -s -X DELETE -H "X-Auth-Email: $CF_AUTH_EMAIL" -H "X-Auth-Key: $CF_AUTH_KEY" \
|
||||
-H "Content-Type: application/json" \
|
||||
"https://api.cloudflare.com/client/v4/zones/5e7e9286849525abf7f30b451b7964ac/purge_cache" \
|
||||
-d '{"purge_everything":true}'
|
||||
```
|
||||
|
||||
### Active DNS Records (gohorsejobs.com)
|
||||
|
||||
| Subdomain | Type | IP/Target | Proxied |
|
||||
|-----------|------|------------|---------|
|
||||
| dev.gohorsejobs.com | A | 38.19.201.52 | No |
|
||||
| api.gohorsejobs.com | A | 86.48.29.139 | Yes |
|
||||
| api-dev.gohorsejobs.com | A | 86.48.29.139 | Yes |
|
||||
| api-local.gohorsejobs.com | A | 38.19.201.52 | No |
|
||||
| b-local.gohorsejobs.com | A | 38.19.201.52 | No |
|
||||
| s-local.gohorsejobs.com | A | 38.19.201.52 | No |
|
||||
| coolify-dev.gohorsejobs.com | A | 185.194.141.70 | No |
|
||||
| local.gohorsejobs.com | A | 185.194.141.70 | No |
|
||||
| api-local.gohorsejobs.com | A | 185.194.141.70 | No |
|
||||
| b-local.gohorsejobs.com | A | 185.194.141.70 | No |
|
||||
| s-local.gohorsejobs.com | A | 185.194.141.70 | No |
|
||||
| panel.gohorsejobs.com | A | Multiple (Load Balanced) | Yes |
|
||||
| pipe.gohorsejobs.com | A | Multiple (Load Balanced) | Yes |
|
||||
| alert.gohorsejobs.com | A | Multiple (Load Balanced) | Yes |
|
||||
| task.gohorsejobs.com | A | Multiple (Load Balanced) | Yes |
|
||||
| stats.gohorsejobs.com | A | Multiple (Load Balanced) | Yes |
|
||||
| storage.gohorsejobs.com | A | Multiple (Load Balanced) | Yes |
|
||||
| base.gohorsejobs.com | A | Multiple | No |
|
||||
| reg.gohorsejobs.com | A | Multiple (Load Balanced) | Yes |
|
||||
| gohorsejobs.com | CNAME | gohorsejobs.pages.dev | Yes |
|
||||
| *.gohorsejobs.com | CNAME | 8a3f435b-f374-4268-90f7-5610f577c706.cfargotunnel.com | Yes |
|
||||
| mail.gohorsejobs.com | CNAME | everest.mxrouting.net | No |
|
||||
|
||||
> Total: 190 DNS records (paginados)
|
||||
|
||||
---
|
||||
|
||||
## ☁️ Coolify DEV Environment (Redbull)
|
||||
|
||||
Ambiente de desenvolvimento no Coolify para deploy automatizado via Git.
|
||||
|
||||
### Server Info
|
||||
|
||||
| Property | Value |
|
||||
|----------|-------|
|
||||
| **Host** | redbull (185.194.141.70) |
|
||||
| **Coolify URL** | https://redbull.rede5.com.br |
|
||||
| **API Token** | `~/.ssh/coolify-redbull-token` |
|
||||
| **SSH Key** | `~/.ssh/civo` |
|
||||
| **Project UUID** | `gkgksco0ow4kgwo8ow4cgs8c` |
|
||||
| **Environment** | `dev` |
|
||||
|
||||
### Resources Created
|
||||
|
||||
| Resource | UUID | Port | Domain | Status |
|
||||
|----------|------|------|--------|--------|
|
||||
| Backend | `iw4sow8s0kkg4cccsk08gsoo` | 8521 | https://api-local.gohorsejobs.com | running |
|
||||
| Frontend | `ao8g40scws0w4cgo8coc8o40` | 3000 | https://local.gohorsejobs.com | running |
|
||||
| Backoffice | `hg48wkw4wggwsswcwc8sooo4` | 3001 | https://b-local.gohorsejobs.com | running |
|
||||
| Seeder | `q4w48gos8cgssso00o8w8gck` | 8080 | https://s-local.gohorsejobs.com | running:healthy |
|
||||
| Database (PostgreSQL) | `bgws48os8wgwk08o48wg8k80` | 5432 | Internal only | running:healthy |
|
||||
|
||||
### API Reference
|
||||
|
||||
**Base URL:** `https://redbull.rede5.com.br/api/v1`
|
||||
|
||||
**Server UUID:** `m844o4gkwkwcc0k48swgs8c8`
|
||||
|
||||
```bash
|
||||
# Listar aplicações
|
||||
curl -s -H "Authorization: Bearer $(cat ~/.ssh/coolify-redbull-token)" \
|
||||
"https://redbull.rede5.com.br/api/v1/applications"
|
||||
|
||||
# Atualizar domínios (requer http:// ou https://)
|
||||
curl -s -X PATCH -H "Authorization: Bearer $(cat ~/.ssh/coolify-redbull-token)" \
|
||||
-H "Content-Type: application/json" \
|
||||
"https://redbull.rede5.com.br/api/v1/applications/<UUID>" \
|
||||
-d '{"domains":"http://local.gohorsejobs.com","instant_deploy":true}'
|
||||
|
||||
# Deploy aplicação
|
||||
curl -s -H "Authorization: Bearer $(cat ~/.ssh/coolify-redbull-token)" \
|
||||
"https://redbull.rede5.com.br/api/v1/deploy?uuid=<UUID>"
|
||||
|
||||
# Ver domínios do servidor
|
||||
curl -s -H "Authorization: Bearer $(cat ~/.ssh/coolify-redbull-token)" \
|
||||
"https://redbull.rede5.com.br/api/v1/servers/m844o4gkwkwcc0k48swgs8c8/domains"
|
||||
```
|
||||
|
||||
### Architecture
|
||||
|
||||
```
|
||||
GitHub (rede5/gohorsejobs.git) ←→ Forgejo (pipe.gohorsejobs.com)
|
||||
│ │
|
||||
▼ ▼
|
||||
Coolify (redbull.rede5.com.br)
|
||||
├── Traefik (reverse proxy + TLS via Let's Encrypt)
|
||||
│
|
||||
├── gohorsejobs-backend-dev → https://api-local.gohorsejobs.com
|
||||
├── gohorsejobs-frontend-dev → https://local.gohorsejobs.com
|
||||
├── gohorsejobs-backoffice-dev → https://b-local.gohorsejobs.com
|
||||
├── gohorsejobs-seeder-dev → https://s-local.gohorsejobs.com
|
||||
│
|
||||
└── PostgreSQL 16 (gohorsejobs-dev) → Internal network only
|
||||
```
|
||||
|
||||
### Environment Variables
|
||||
|
||||
Configured via Coolify UI or API:
|
||||
|
||||
```bash
|
||||
DATABASE_URL=postgres://gohorsejobs:gohorsejobs123@bgws48os8wgwk08o48wg8k80:5432/gohorsejobs?sslmode=disable
|
||||
BACKEND_PORT=8521
|
||||
ENV=development
|
||||
JWT_SECRET=gohorsejobs-dev-jwt-secret-2024-very-secure-key-32ch
|
||||
JWT_EXPIRATION=7d
|
||||
PASSWORD_PEPPER=gohorse-pepper
|
||||
COOKIE_SECRET=gohorsejobs-cookie-secret-dev
|
||||
CORS_ORIGINS=https://local.gohorsejobs.com,https://b-local.gohorsejobs.com,http://localhost:3000,http://localhost:8521
|
||||
```
|
||||
|
||||
> ⚠️ **`PASSWORD_PEPPER` é crítico.** Todas as migration seeds e o seeder-api usam `gohorse-pepper`.
|
||||
> Se este valor for alterado no Coolify sem regravar os hashes no banco, **todos os logins falharão**
|
||||
> com `invalid credentials`. Veja a seção de troubleshooting abaixo.
|
||||
|
||||
### ⚠️ Troubleshooting: Login retorna `invalid credentials`
|
||||
|
||||
**Causa:** O `PASSWORD_PEPPER` no Coolify não coincide com o pepper usado para gerar os hashes no banco.
|
||||
|
||||
**Diagnóstico via SSH:**
|
||||
```bash
|
||||
ssh redbull
|
||||
# Verificar pepper no container em execução:
|
||||
docker inspect <backend_container_name> --format '{{range .Config.Env}}{{println .}}{{end}}' | grep PEPPER
|
||||
|
||||
# Testar login direto (sem escaping de shell):
|
||||
cat > /tmp/login.json <<'EOF'
|
||||
{"email":"lol","password":"Admin@2025!"}
|
||||
EOF
|
||||
docker run --rm --network coolify -v /tmp/login.json:/tmp/login.json \
|
||||
curlimages/curl:latest -s -X POST \
|
||||
http://<backend_container>:8521/api/v1/auth/login \
|
||||
-H 'Content-Type: application/json' -d @/tmp/login.json
|
||||
```
|
||||
|
||||
**Fix — opção 1 (preferível): corrigir o pepper no Coolify e re-rodar o seeder:**
|
||||
```bash
|
||||
TOKEN=$(cat ~/.ssh/coolify-redbull-token)
|
||||
|
||||
# 1. Atualizar PASSWORD_PEPPER
|
||||
curl -s -X PATCH \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"key":"PASSWORD_PEPPER","value":"gohorse-pepper"}' \
|
||||
"https://redbull.rede5.com.br/api/v1/applications/iw4sow8s0kkg4cccsk08gsoo/envs"
|
||||
|
||||
# 2. Reiniciar o backend (para ele pegar o novo pepper)
|
||||
curl -s -H "Authorization: Bearer $TOKEN" \
|
||||
"https://redbull.rede5.com.br/api/v1/applications/iw4sow8s0kkg4cccsk08gsoo/restart"
|
||||
|
||||
# 3. Re-rodar o seeder (ele regrava o hash com o pepper correto automaticamente)
|
||||
curl -s -H "Authorization: Bearer $TOKEN" \
|
||||
"https://redbull.rede5.com.br/api/v1/deploy?uuid=q4w48gos8cgssso00o8w8gck"
|
||||
```
|
||||
|
||||
> O seeder (`seedUsers()`) sempre faz upsert do superadmin/lol com `bcrypt(senha + PEPPER)`.
|
||||
> Mudar o pepper e re-rodar o seeder é suficiente — nenhuma migration precisa ser tocada.
|
||||
|
||||
**Fix — opção 2 (emergência, sem seeder): regravar hash direto no banco:**
|
||||
```bash
|
||||
ssh redbull
|
||||
|
||||
# Gerar novo hash com node (usando arquivo para evitar expansão de $ pelo shell):
|
||||
mkdir -p /tmp/hashgen && cat > /tmp/hashgen/gen.js <<'EOF'
|
||||
const b = require("./node_modules/bcryptjs");
|
||||
console.log(b.hashSync("Admin@2025!" + process.env.PEPPER, 10));
|
||||
EOF
|
||||
docker run --rm -v /tmp/hashgen:/app -w /app -e PEPPER=gohorse-pepper \
|
||||
node:20-alpine sh -c "npm install bcryptjs -s && node gen.js"
|
||||
|
||||
# Aplicar no banco (SEMPRE usar -f, nunca -c, para preservar os $ do hash):
|
||||
cat > /tmp/fix_hash.sql <<'EOF'
|
||||
UPDATE users SET password_hash = '<hash_gerado_acima>', status = 'active'
|
||||
WHERE identifier IN ('lol', 'superadmin');
|
||||
EOF
|
||||
docker cp /tmp/fix_hash.sql bgws48os8wgwk08o48wg8k80:/tmp/fix_hash.sql
|
||||
docker exec bgws48os8wgwk08o48wg8k80 psql -U gohorsejobs -d gohorsejobs -f /tmp/fix_hash.sql
|
||||
```
|
||||
|
||||
> ⚠️ **Nunca passe hash bcrypt via `-c '...'`** na linha de comando — o shell expande os `$`
|
||||
> e corrompe o hash silenciosamente. Use sempre um arquivo e `-f`.
|
||||
|
||||
### Deploy via API
|
||||
|
||||
```bash
|
||||
# Deploy application
|
||||
curl -H "Authorization: Bearer $(cat ~/.ssh/coolify-redbull-token)" \
|
||||
"https://redbull.rede5.com.br/api/v1/deploy?uuid=iw4sow8s0kkg4cccsk08gsoo"
|
||||
|
||||
# Check deployment status
|
||||
curl -H "Authorization: Bearer $(cat ~/.ssh/coolify-redbull-token)" \
|
||||
"https://redbull.rede5.com.br/api/v1/deployments/<deployment_uuid>"
|
||||
|
||||
# List applications
|
||||
curl -H "Authorization: Bearer $(cat ~/.ssh/coolify-redbull-token)" \
|
||||
"https://redbull.rede5.com.br/api/v1/applications"
|
||||
|
||||
# List databases
|
||||
curl -H "Authorization: Bearer $(cat ~/.ssh/coolify-redbull-token)" \
|
||||
"https://redbull.rede5.com.br/api/v1/databases"
|
||||
```
|
||||
|
||||
### Coolify Reference
|
||||
|
||||
- **Docs:** https://coolify.io/docs/get-started/introduction
|
||||
- **API Reference:** https://coolify.io/docs/api-reference/authorization
|
||||
- **GitHub Integration:** Uses SSH deploy key for private repo access
|
||||
This document maps out the comprehensive DevOps lifecycle, the server topologies, the container orchestrations, and CI/CD operations powering GoHorseJobs.
|
||||
|
||||
---
|
||||
|
||||
## 🏗️ Architecture Diagrams
|
||||
|
||||
### Full Infrastructure Overview
|
||||
### 1. Global Infrastructure Overview (DEV / HML Environments)
|
||||
A look into how our development environment handles requests through Cloudflare down to the Coolify-managed `Redbull` VPS.
|
||||
|
||||
```mermaid
|
||||
graph TB
|
||||
subgraph Clients ["Clients"]
|
||||
Browser["Browser / Mobile"]
|
||||
subgraph Clients ["Public Clients"]
|
||||
Browser["Web Browser / Mobile App"]
|
||||
end
|
||||
|
||||
subgraph CF ["Cloudflare (DNS + CDN)"]
|
||||
subgraph CF ["Cloudflare (DNS + Proxy + CDN)"]
|
||||
DNS["DNS Zone: gohorsejobs.com"]
|
||||
WAF["Web Application Firewall (WAF)"]
|
||||
end
|
||||
|
||||
subgraph Redbull ["Redbull VPS (185.194.141.70) — Coolify DEV"]
|
||||
TraefikR("Traefik + Let's Encrypt")
|
||||
subgraph Redbull ["Redbull VPS (185.194.141.70) — Ubuntu"]
|
||||
TraefikR("Traefik (Reverse Proxy + Let's Encrypt)")
|
||||
|
||||
subgraph CoolifyApps ["Coolify Applications"]
|
||||
FE_C["Frontend (:3000)"]
|
||||
BE_C["Backend API (:8521)"]
|
||||
BO_C["Backoffice (:3001)"]
|
||||
SE_C["Seeder API (:8080)"]
|
||||
subgraph CoolifyApps ["Coolify Application Containers"]
|
||||
FE_C["Frontend (Next.js)"]
|
||||
BE_C["Backend API (Go)"]
|
||||
BO_C["Backoffice (NestJS)"]
|
||||
SE_C["Seeder API (Node.js)"]
|
||||
end
|
||||
|
||||
PG_C[("PostgreSQL 16\ngohorsejobs-dev")]
|
||||
subgraph CoolifyData ["Coolify Data Containers"]
|
||||
PG_C[("PostgreSQL 16")]
|
||||
MQ_C["LavinMQ / RabbitMQ"]
|
||||
Redis_C[("Redis (Caching/Sessions)")]
|
||||
end
|
||||
end
|
||||
|
||||
subgraph Apolo ["Apolo VPS (38.19.201.52) — Podman/Quadlet"]
|
||||
TraefikA("Traefik")
|
||||
Browser -->|HTTPS| DNS
|
||||
DNS --> WAF
|
||||
WAF -->|Proxy/Cache| TraefikR
|
||||
TraefikR -->|local.gohorsejobs.com| FE_C
|
||||
TraefikR -->|api-local.gohorsejobs.com| BE_C
|
||||
TraefikR -->|b-local.gohorsejobs.com| BO_C
|
||||
|
||||
subgraph PodmanApps ["Podman Containers (Systemd/Quadlet)"]
|
||||
FE_A["Frontend (:3000)"]
|
||||
BE_A["Backend API (:8521)"]
|
||||
BO_A["Backoffice (:3001)"]
|
||||
SE_A["Seeder API (:8080)"]
|
||||
end
|
||||
|
||||
PG_A[("PostgreSQL\npostgres-main")]
|
||||
Storage["/mnt/data\n(configs + DB data)"]
|
||||
end
|
||||
|
||||
subgraph Git ["Git Repositories"]
|
||||
GH["GitHub\nrede5/gohorsejobs"]
|
||||
FJ["Forgejo (pipe)\npipe.gohorsejobs.com"]
|
||||
end
|
||||
|
||||
subgraph External ["External Services"]
|
||||
Stripe["Stripe (Payments)"]
|
||||
Firebase["Firebase (FCM)"]
|
||||
R2["Cloudflare R2 (Storage)"]
|
||||
LavinMQ["LavinMQ (AMQP)"]
|
||||
Resend["Resend (Email)"]
|
||||
end
|
||||
|
||||
%% Client Flow
|
||||
Browser --> DNS
|
||||
DNS -- "local.gohorsejobs.com" --> TraefikR
|
||||
DNS -- "dev.gohorsejobs.com" --> TraefikA
|
||||
|
||||
%% Redbull Routing
|
||||
TraefikR -- "local.gohorsejobs.com" --> FE_C
|
||||
TraefikR -- "api-local.gohorsejobs.com" --> BE_C
|
||||
TraefikR -- "b-local.gohorsejobs.com" --> BO_C
|
||||
TraefikR -- "s-local.gohorsejobs.com" --> SE_C
|
||||
BE_C --> PG_C
|
||||
BO_C --> PG_C
|
||||
SE_C --> PG_C
|
||||
|
||||
%% Apolo Routing
|
||||
TraefikA -- "dev.gohorsejobs.com" --> FE_A
|
||||
TraefikA -- "api-tmp.gohorsejobs.com" --> BE_A
|
||||
TraefikA -- "b-tmp.gohorsejobs.com" --> BO_A
|
||||
BE_A --> PG_A
|
||||
BO_A --> PG_A
|
||||
SE_A --> PG_A
|
||||
PG_A -.-> Storage
|
||||
|
||||
%% Git Flow
|
||||
GH <--> FJ
|
||||
|
||||
%% External
|
||||
BE_C -.-> Stripe
|
||||
BE_C -.-> Firebase
|
||||
BE_C -.-> R2
|
||||
BO_C -.-> LavinMQ
|
||||
BO_C -.-> Resend
|
||||
|
||||
style PG_C fill:#336791,stroke:#fff,color:#fff
|
||||
style PG_A fill:#336791,stroke:#fff,color:#fff
|
||||
style TraefikR fill:#f5a623,stroke:#fff,color:#fff
|
||||
style TraefikA fill:#f5a623,stroke:#fff,color:#fff
|
||||
style CF fill:#f48120,stroke:#fff,color:#fff
|
||||
FE_C -.->|REST /api/v1| BE_C
|
||||
BO_C -.->|Queries| PG_C
|
||||
BE_C <-->|Queries| PG_C
|
||||
BE_C -.->|AMQP Pub| MQ_C
|
||||
MQ_C -.->|AMQP Sub| BO_C
|
||||
```
|
||||
|
||||
### Apolo VPS (Podman/Quadlet) Detail
|
||||
### 2. CI/CD Operations (Forgejo -> VPS)
|
||||
How code travels from a `git push` on `dev` to the live container.
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
subgraph Host ["Apolo VPS (Host)"]
|
||||
|
||||
subgraph FS ["File System (/mnt/data)"]
|
||||
EnvBE["/gohorsejobs/backend/.env"]
|
||||
EnvBO["/gohorsejobs/backoffice/.env"]
|
||||
EnvSE["/gohorsejobs/seeder-api/.env"]
|
||||
DBData[("postgres-general")]
|
||||
end
|
||||
|
||||
subgraph Net ["Network: web_proxy"]
|
||||
Traefik("Traefik")
|
||||
|
||||
subgraph App ["Application Containers"]
|
||||
BE["Backend API (:8521)"]
|
||||
BO["Backoffice (:3001)"]
|
||||
SE["Seeder API (:8080)"]
|
||||
FE["Frontend (:3000)"]
|
||||
end
|
||||
|
||||
PG[("postgres-main (:5432)")]
|
||||
end
|
||||
end
|
||||
|
||||
%% Ingress
|
||||
Internet((Internet)) --> Traefik
|
||||
|
||||
%% Routing
|
||||
Traefik -- "dev.gohorsejobs.com" --> FE
|
||||
Traefik -- "api-tmp.gohorsejobs.com" --> BE
|
||||
Traefik -- "b-tmp.gohorsejobs.com" --> BO
|
||||
Traefik -- "seeder.gohorsejobs.com" --> SE
|
||||
|
||||
%% Config Mounts
|
||||
EnvBE -.-> BE
|
||||
EnvBO -.-> BO
|
||||
EnvSE -.-> SE
|
||||
|
||||
%% Data Persistence
|
||||
PG -.-> DBData
|
||||
|
||||
%% Database Connections
|
||||
BE --> PG
|
||||
BO --> PG
|
||||
SE --> PG
|
||||
|
||||
style PG fill:#336791,stroke:#fff,color:#fff
|
||||
style Traefik fill:#f5a623,stroke:#fff,color:#fff
|
||||
```
|
||||
|
||||
### Coolify DEV (Redbull) Detail
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
subgraph Redbull ["Redbull VPS — Coolify (redbull.rede5.com.br)"]
|
||||
Traefik("Traefik + Let's Encrypt")
|
||||
|
||||
subgraph Apps ["Applications (auto-deploy via Git)"]
|
||||
BE["Backend Go\n:8521"]
|
||||
FE["Frontend Next.js\n:3000"]
|
||||
BO["Backoffice NestJS\n:3001"]
|
||||
SE["Seeder API\n:8080"]
|
||||
end
|
||||
|
||||
PG[("PostgreSQL 16\ngohorsejobs\n:5432")]
|
||||
end
|
||||
|
||||
GH["GitHub (rede5/gohorsejobs)"] --> |"push dev"| Traefik
|
||||
|
||||
Internet((Internet)) --> Traefik
|
||||
Traefik -- "api-local.gohorsejobs.com" --> BE
|
||||
Traefik -- "local.gohorsejobs.com" --> FE
|
||||
Traefik -- "b-local.gohorsejobs.com" --> BO
|
||||
Traefik -- "s-local.gohorsejobs.com" --> SE
|
||||
|
||||
BE --> PG
|
||||
BO --> PG
|
||||
SE --> PG
|
||||
|
||||
style PG fill:#336791,stroke:#fff,color:#fff
|
||||
style Traefik fill:#f5a623,stroke:#fff,color:#fff
|
||||
```
|
||||
|
||||
### CI/CD Flow (Dual Pipeline)
|
||||
|
||||
Existem **2 pipelines independentes** disparados simultaneamente a cada push:
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
Dev["Developer\ngit push dev"]
|
||||
|
||||
subgraph Pipeline1 ["Pipeline 1: GitHub → Coolify"]
|
||||
GH["GitHub\n(origin)"]
|
||||
Webhook["GitHub Webhook\n(push event)"]
|
||||
Coolify["Coolify\n(redbull.rede5.com.br)"]
|
||||
Redbull["Redbull VPS\nFrontend + Backend + Backoffice + Seeder"]
|
||||
end
|
||||
|
||||
subgraph Pipeline2 ["Pipeline 2: Forgejo → K3s Cluster"]
|
||||
FJ["Forgejo\n(pipe.gohorsejobs.com)"]
|
||||
Runner["Forgejo Actions Runner\n(self-hosted, K3s)"]
|
||||
Registry["Container Registry\npipe.gohorsejobs.com"]
|
||||
K3s["K3s Cluster\nBackend + Backoffice"]
|
||||
end
|
||||
|
||||
Dev --> GH
|
||||
Dev --> FJ
|
||||
|
||||
GH --> Webhook --> Coolify --> |"Docker build"| Redbull
|
||||
|
||||
FJ --> |"push triggers"| Runner
|
||||
Runner --> |"docker build & push"| Registry
|
||||
Runner --> |"kubectl apply"| K3s
|
||||
```
|
||||
|
||||
| Pipeline | Trigger | Servicos | Destino |
|
||||
|----------|---------|----------|---------|
|
||||
| **GitHub → Coolify** | Webhook (push) | Frontend, Backend, Backoffice, Seeder | Redbull VPS (Docker) |
|
||||
| **Forgejo → K3s** | Forgejo Actions (push) | Backend, Backoffice | K3s Cluster (Kubernetes) |
|
||||
|
||||
---
|
||||
|
||||
## 🔄 Forgejo CI/CD Pipeline (pipe.gohorsejobs.com)
|
||||
|
||||
O pipeline roda automaticamente via Forgejo Actions a cada push na branch `dev`.
|
||||
|
||||
### Workflow: `.forgejo/workflows/deploy.yaml`
|
||||
|
||||
| Job | Descricao | Status Atual |
|
||||
|-----|-----------|-------------|
|
||||
| **build-and-push** | Build Docker images (backend + backoffice), push to registry | OK |
|
||||
| **deploy** | Deploy ao K3s via kubectl (requer KUBECONFIG secret) | OK (fix: KUBE_CONFIG → KUBECONFIG) |
|
||||
|
||||
### Pipeline Steps
|
||||
|
||||
1. **build-and-push** (OK):
|
||||
- Checkout code
|
||||
- Docker login no registry `pipe.gohorsejobs.com`
|
||||
- Build & push backend: `pipe.gohorsejobs.com/bohessefm/gohorsejobs:latest`
|
||||
- Build & push backoffice: `pipe.gohorsejobs.com/bohessefm/backoffice:latest`
|
||||
|
||||
2. **deploy** (FAIL - K3s nao configurado):
|
||||
- Install kubectl
|
||||
- Configure kubeconfig (via `secrets.KUBE_CONFIG`)
|
||||
- Sync secrets e vars ao namespace `gohorsejobsdev`
|
||||
- `kubectl apply -f k8s/dev/`
|
||||
- Set image com SHA do commit
|
||||
- Rollout restart deployments
|
||||
|
||||
> **Nota:** O job deploy usava `secrets.KUBE_CONFIG` mas o secret se chama `KUBECONFIG`. Corrigido no commit atual.
|
||||
|
||||
### Forgejo Actions Secrets & Variables
|
||||
|
||||
**Secrets** (configurados em Settings > Actions > Secrets):
|
||||
- `FORGEJO_TOKEN` — Login no container registry
|
||||
- `KUBECONFIG` — Kubeconfig para acesso ao K3s cluster
|
||||
|
||||
**Variables** (configurados em Settings > Actions > Variables):
|
||||
- `DATABASE_URL`, `JWT_SECRET`, `PASSWORD_PEPPER`, `COOKIE_SECRET`, `COOKIE_DOMAIN`
|
||||
- `BACKEND_PORT`, `BACKEND_HOST`, `ENV`, `CORS_ORIGINS`, `MTU`
|
||||
- `AMQP_URL`, `S3_BUCKET`, `AWS_REGION`, `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, `AWS_ENDPOINT`
|
||||
- `RSA_PRIVATE_KEY_BASE64`, `JWT_EXPIRATION`
|
||||
|
||||
### Forgejo API
|
||||
|
||||
```bash
|
||||
# Token location: ~/.ssh/forgejo-token
|
||||
FORGEJO_TOKEN="03d23c54672519c8473bd9c46ae7820b13c8b287"
|
||||
|
||||
# Listar runs do pipeline
|
||||
curl -s -H "Authorization: token $FORGEJO_TOKEN" \
|
||||
"https://pipe.gohorsejobs.com/api/v1/repos/bohessefm/gohorsejobs/actions/tasks?limit=5"
|
||||
|
||||
# Listar repositorios
|
||||
curl -s -H "Authorization: token $FORGEJO_TOKEN" \
|
||||
"https://pipe.gohorsejobs.com/api/v1/user/repos"
|
||||
```
|
||||
|
||||
### GitHub Webhooks (Auto-deploy Coolify)
|
||||
|
||||
Webhooks configurados no GitHub apontando para o Coolify:
|
||||
|
||||
| App | Webhook URL |
|
||||
|-----|-------------|
|
||||
| Backend | `https://redbull.rede5.com.br/webhooks/source/github/events/manual?uuid=iw4sow8s0kkg4cccsk08gsoo&secret=...` |
|
||||
| Frontend | `https://redbull.rede5.com.br/webhooks/source/github/events/manual?uuid=ao8g40scws0w4cgo8coc8o40&secret=...` |
|
||||
| Backoffice | `https://redbull.rede5.com.br/webhooks/source/github/events/manual?uuid=hg48wkw4wggwsswcwc8sooo4&secret=...` |
|
||||
| Seeder | `https://redbull.rede5.com.br/webhooks/source/github/events/manual?uuid=q4w48gos8cgssso00o8w8gck&secret=...` |
|
||||
|
||||
---
|
||||
|
||||
## 🔑 Credenciais e Tokens (Referencias)
|
||||
|
||||
Todos os tokens estao armazenados em `~/.ssh/`:
|
||||
|
||||
| Arquivo | Servico | Uso |
|
||||
|---------|---------|-----|
|
||||
| `~/.ssh/coolify-redbull-token` | Coolify API | Deploy e gerenciamento de apps |
|
||||
| `~/.ssh/forgejo-token` | Forgejo API (pipe) | CI/CD, webhooks, repos |
|
||||
| `~/.ssh/github-token` | GitHub API | Webhooks, repos |
|
||||
| `~/.ssh/cloudflare-token` | Cloudflare API | DNS, cache |
|
||||
| `~/.ssh/absam-token` | Absam Cloud API | VPS management |
|
||||
| `~/.ssh/forgejo-gohorsejobs` | SSH Key | Forgejo Git operations |
|
||||
| `~/.ssh/civo` | SSH Key | Acesso VPS Redbull |
|
||||
| `~/.ssh/github` | SSH Key | GitHub Git operations |
|
||||
|
||||
---
|
||||
|
||||
## 💾 Storage & Persistence (`/mnt/data`)
|
||||
|
||||
All persistent data and configuration files are stored in `/mnt/data` on the host.
|
||||
|
||||
| Host Path | Container Path | Purpose | Type |
|
||||
|-----------|----------------|---------|------|
|
||||
| `/mnt/data/gohorsejobs/backend/.env` | (Injected Env) | **Backend Config:** Secrets, DB URL, Port settings. | File |
|
||||
| `/mnt/data/gohorsejobs/backoffice/.env` | (Injected Env) | **Backoffice Config:** Secrets, DB URL. | File |
|
||||
| `/mnt/data/gohorsejobs/seeder-api/.env` | (Injected Env) | **Seeder Config:** Secrets, DB URL. | File |
|
||||
| `/mnt/data/postgres-general` | `/var/lib/postgresql/data` | **Database Storage:** Main storage for `postgres-main` container. Contains `gohorsejobs_dev` DB. | Directory |
|
||||
|
||||
> **Backup Note:** To backup the environment, ensure `/mnt/data/gohorsejobs` and `/mnt/data/postgres-general` are included in snapshots.
|
||||
|
||||
---
|
||||
|
||||
## 🌍 Service Maps & Networking
|
||||
|
||||
### 🚦 Traefik Routing
|
||||
Services are exposed via Traefik labels defined in the Quadlet `.container` files.
|
||||
|
||||
| Domain | Service | Internal Port | Host Port (Debug) |
|
||||
|--------|---------|---------------|-------------------|
|
||||
| `dev.gohorsejobs.com` | `gohorsejobs-frontend-dev` | `3000` | `8523` |
|
||||
| `api-tmp.gohorsejobs.com` | `gohorsejobs-backend-dev` | `8521` | `8521` |
|
||||
| `b-tmp.gohorsejobs.com` | `gohorsejobs-backoffice-dev` | `3001` | - |
|
||||
| `seeder.gohorsejobs.com` | `gohorsejobs-seeder-dev` | `8080` | `8522` |
|
||||
|
||||
### 🛑 Security
|
||||
- **Backend/Seeder/Frontend** expose ports to the Host (`85xx`) for debugging/direct access if needed.
|
||||
- **Backoffice** is *only* accessible via Traefik (internal network).
|
||||
- **PostgreSQL** is *only* accessible internally via `web_proxy` network (no host port binding).
|
||||
|
||||
---
|
||||
|
||||
## 🛠️ Operational Guide
|
||||
|
||||
### 1. View & Manage Configs
|
||||
Configurations are **not** inside containers. Edit them on the host:
|
||||
```bash
|
||||
# Edit Backend Config
|
||||
vim /mnt/data/gohorsejobs/backend/.env
|
||||
|
||||
# Apply changes
|
||||
systemctl restart gohorsejobs-backend-dev
|
||||
```
|
||||
|
||||
### 2. Full Environment Restart
|
||||
To restart all GoHorseJobs related services (excluding Database):
|
||||
```bash
|
||||
systemctl restart gohorsejobs-backend-dev gohorsejobs-backoffice-dev gohorsejobs-seeder-dev gohorsejobs-frontend-dev
|
||||
```
|
||||
|
||||
### 3. Database Access
|
||||
Access the local database directly via the `postgres-main` container:
|
||||
```bash
|
||||
# Internal Connection
|
||||
docker exec -it postgres-main psql -U yuki -d gohorsejobs_dev
|
||||
sequenceDiagram
|
||||
participant Dev as Developer
|
||||
participant Git as GitHub (Origin)
|
||||
participant Forgejo as Pipe / Forgejo (CI)
|
||||
participant Coolify as Coolify Webhook
|
||||
participant VPS as Redbull (VPS)
|
||||
|
||||
Dev->>Git: git push origin dev
|
||||
Dev->>Forgejo: git push pipe dev
|
||||
Note over Forgejo: Trigger Action (.forgejo/workflows/)
|
||||
Forgejo->>Coolify: POST Deploy Webhook
|
||||
Coolify-->>VPS: Fetch latest dev branch
|
||||
Note over VPS: Coolify builds Nixpacks/Dockerfile
|
||||
Coolify-->>VPS: docker stop <old_containers>
|
||||
Coolify-->>VPS: docker run <new_containers>
|
||||
Coolify-->>Forgejo: Deployment Success
|
||||
Forgejo-->>Dev: Pipeline Green/Passed
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Deployment Pipeline (Manual)
|
||||
## 🛠️ Environments Topology
|
||||
|
||||
Current workflow uses **Local Build** -> **Forgejo Registry** -> **Server Pull**.
|
||||
| Environment | Branch | Use Case | Server Host | Reverse Proxy | Config Manager | Domains |
|
||||
| :--- | :--- | :--- | :--- | :--- | :--- | :--- |
|
||||
| **Local (Host)** | N/A | Developer Sandbox | Laptop / PC | None | `start.sh` (Bare metal) | `localhost:8963` |
|
||||
| **DEV** | `dev` | Continuous Integration | `Redbull` VPS | Traefik | Coolify v4 | `local.`, `api-local.`, `b-local.` |
|
||||
| **HML** | `hml` | QA / Staging Testing | `Apolo` VPS | Nginx proxy | Podman (Quadlet) | `hml.`, `api-hml.`, `b-hml.` |
|
||||
| **PRD** | `main` | Production Live | `Zeus`/`Poseidon` | Traefik Ingress | Kubernetes (K3s) | `gohorsejobs.com`, `api.` |
|
||||
|
||||
### 1. Build & Push (Local Machine)
|
||||
---
|
||||
|
||||
## 🔧 Coolify Instance (Redbull)
|
||||
|
||||
The `dev` branch automatically mirrors to the Redbull server (185.194.141.70) managed by Coolify.
|
||||
* **Coolify Interface:** `https://redbull.rede5.com.br`
|
||||
* **GitHub Integration:** Relies on an SSH deployment key injected into the Forgejo actions.
|
||||
|
||||
### Container Rules
|
||||
* Never manually `docker run` on Redbull. Use the Coolify interface to add environment variables or alter build commands.
|
||||
* **Secrets:** Managed via Coolify Environment Variables. (e.g., `PASSWORD_PEPPER`, `JWT_SECRET`).
|
||||
|
||||
---
|
||||
|
||||
## 💡 Troubleshooting & Known Faultlines
|
||||
|
||||
### 1. `Invalid Credentials` Right After DB Seed
|
||||
If the Backend Go server complains about invalid passwords right after you run `npm run seed`:
|
||||
1. Check the `PASSWORD_PEPPER` inside the Coolify instance for the `seeder-api`.
|
||||
2. It **must exactly match** the pepper configured for the Backend API.
|
||||
3. If they matched, rerun `npm run seed` via the Coolify interface to force hash recalculation over the raw DB rows.
|
||||
|
||||
### 2. Out of Memory (OOMKilled) on Build
|
||||
Node.js (for Next.js and NestJS) and Go can eat a lot of RAM during concurrent Coolify builds.
|
||||
* **Fix:** Ensure the Server Actions inside Coolify are set to stagger deployments, preventing out-of-memory cascading crashes on Redbull.
|
||||
|
||||
### 3. SSH Connectivity
|
||||
```bash
|
||||
# Login
|
||||
podman login forgejo-gru.rede5.com.br
|
||||
|
||||
# Build
|
||||
cd backend
|
||||
podman build -t forgejo-gru.rede5.com.br/rede5/gohorsejobs-backend:latest .
|
||||
|
||||
# Push
|
||||
podman push forgejo-gru.rede5.com.br/rede5/gohorsejobs-backend:latest
|
||||
```
|
||||
|
||||
### 2. Deploy (On Apolo Server)
|
||||
```bash
|
||||
ssh root@apolo
|
||||
|
||||
# Pull new image
|
||||
podman pull forgejo-gru.rede5.com.br/rede5/gohorsejobs-backend:latest
|
||||
|
||||
# Restart service (Systemd handles container recreation)
|
||||
systemctl restart gohorsejobs-backend-dev
|
||||
# Connecting to Redbull
|
||||
ssh root@185.194.141.70 -p 22
|
||||
```
|
||||
If access is denied, ensure your local public key is registered in `~/.ssh/authorized_keys` or injected via the VPS admin panel.
|
||||
|
|
|
|||
|
|
@ -1,7 +1,48 @@
|
|||
# Project Documentation - GoHorse Jobs
|
||||
# 📚 Project Documentation - GoHorseJobs
|
||||
|
||||
This directory holds internal team documentation, guides, and extended context regarding project structure and architecture.
|
||||
Welcome to the central documentation hub for **GoHorseJobs**. This repository contains the collective knowledge, architecture decisions, and operational guides for our B2B SaaS recruitment platform.
|
||||
|
||||
## 🚨 AI Rules Warning 🚨
|
||||
Before making ANY changes to infrastructure deployment manifests or Kubernetes files, please refer to the project-wide AI Rules located at `.agent/rules.md`.
|
||||
**Do NOT touch `k3s` or `k8s` files.** Do not alter raw RSA/base64 authentication keys.
|
||||
---
|
||||
|
||||
## 🧭 Navigation & Index
|
||||
|
||||
Choose a specific domain below to dive deep into our technical implementation and guides.
|
||||
|
||||
### 🤖 1. AI & Developer Directives
|
||||
* **[Context for AI Agents (AGENTS.md)](AGENTS.md)**: The supreme source of truth for Claude, Cursor, and other AI coding assistants. Contains stack rules, passwords, gotchas, and layout contexts.
|
||||
* **[Rules (.agent/rules.md)](../.agent/rules.md)**: Absolute system boundaries and deployment limits.
|
||||
|
||||
### 🏗️ 2. High-Level Architecture
|
||||
* **[DevOps & Infrastructure (DEVOPS.md)](DEVOPS.md)**: Full mapping of Cloudflare DNS, Traefik, VPS (Redbull/Apolo), Docker/Coolify containers, and CI/CD pipelines (Forgejo/Drone). Includes rich Mermaid diagrams.
|
||||
* **[Database Schema (DATABASE.md)](DATABASE.md)**: PostgreSQL schemas, relationships, UUID v7 indexing strategies, and ERD visualizing the core data flow.
|
||||
|
||||
### 🔌 3. Application Interfaces (APIs)
|
||||
* **[API Routes (API.md)](API.md)**: Endpoints mapped for the Go Backend (`/api/v1`), NestJS Backoffice services, and internal Node.js Seeder-API.
|
||||
* **[API Security (API_SECURITY.md)](API_SECURITY.md)**: Details on HS256 JWT implementations, RBAC (Role-Based Access Control) levels, and CORS policies.
|
||||
* **[AppSec Strategy (APPSEC_STRATEGY.md)](APPSEC_STRATEGY.md)**: The core mitigation plan against XSS, IDOR, Mass Assignment, and testing vectors within Next.js.
|
||||
|
||||
### 🫂 4. Operations & Testing
|
||||
* **[Test Users & Data (TEST_USERS.md)](TEST_USERS.md)**: Comprehensive list of robust local credentials, passwords, dummy candidates, and seeded companies.
|
||||
* **[Deployment Routes (WORKFLOWS.md)](WORKFLOWS.md)**: A catalog of our `.forgejo` and GitHub Actions synchronizations.
|
||||
* **[Tasks (TASKS.md)](TASKS.md)**: Open checklist of internal improvements.
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Quick Launch (Dev)
|
||||
|
||||
If you are a new developer or setting up the environment post-clone, rely on our interactive script:
|
||||
|
||||
```bash
|
||||
cd /path/to/gohorsejobs
|
||||
./start.sh
|
||||
```
|
||||
|
||||
**Options overview**:
|
||||
* `1`: General Start (Frontend + Backend)
|
||||
* `2`: Fresh Start (Reset Postgres Data -> Run Migrations -> Seed Core Data -> Start)
|
||||
* `3`: God Mode (Frontend + Backend + Backoffice)
|
||||
|
||||
---
|
||||
|
||||
## 🚨 Final Notice
|
||||
**Do NOT** alter base configuration settings (such as encryption secrets, or Kubernetes `k3s`/`k8s` manifests) unless explicitly guided by the Lead Engineer or following the strict protocols in `.agent/rules.md`.
|
||||
|
|
|
|||
|
|
@ -1,6 +1,55 @@
|
|||
# Test Users - GoHorseJobs
|
||||
# 🧪 Test Users & Data Scenarios - GoHorseJobs
|
||||
|
||||
> **DEPRECATED / MOVED**
|
||||
> Este arquivo foi depreciado e toda a documentação das matrizes de Usuários de Teste foi movida e unificada diretamente no **`[README.md](../README.md)`** da raiz do projeto.
|
||||
>
|
||||
> As rotas antigas do `superadmin` foram aposentadas via migrations da base, onde o perfil oficial master passa a se chamar unicamente: `lol`.
|
||||
When running the platform locally (via `start.sh`) or on the DEV server (`local.gohorsejobs.com`), the `seeder-api` provisions a rich set of test accounts representing distinct personas.
|
||||
|
||||
All accounts use the exact same password to simplify development.
|
||||
|
||||
**Universal Test Password:** `Admin@2025!`
|
||||
|
||||
---
|
||||
|
||||
## 👥 Core Personas (The "Golden" Accounts)
|
||||
|
||||
These are the primary accounts you should use for daily development and E2E testing.
|
||||
|
||||
| Role | Identifier (Username) | Email | Purpose |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| **Superadmin** | `lol` | `lol@gohorsejobs.com` | Complete system access. Bypass company walls. Manage features. |
|
||||
| **Superadmin** | `superadmin` | `admin@gohorsejobs.com` | Secondary global admin. |
|
||||
| **Admin** | `admin` | `moderator@gohorsejobs.com` | Reviews pending companies/jobs in backoffice. |
|
||||
| **Recruiter** | `recruiter` | `hr@techcorp.com` | Posts jobs, manages applicants for "TechCorp" (Company ID: 1). |
|
||||
| **Recruiter** | `jane_hr` | `jane.doe@startup.io` | Recruiter for a pending/unapproved company to test gating. |
|
||||
| **Candidate** | `candidate` | `carlos.dev@gmail.com` | Standard job seeker. Pre-filled resume and skills. Has 3 active applications. |
|
||||
| **Candidate** | `newbie` | `new.user@hotmail.com` | Fresh account with 0 applications and empty profile to test onboarding flows. |
|
||||
|
||||
---
|
||||
|
||||
## 🏢 Auto-Generated Companies & Jobs
|
||||
|
||||
The DB seeder simulates a healthy marketplace.
|
||||
If you run `npm run seed:lite`, the DB receives:
|
||||
* **50 Companies**: Ranging from "TechCorp" (Active) to "SuspiciousLTDA" (Pending/Rejected).
|
||||
* **100 Jobs**: Distributed among the active companies. Various statuses (`published`, `draft`, `closed`).
|
||||
* **200 Applications**: Dummy candidates applying to random jobs, in varied pipelines (`pending`, `reviewing`, `accepted`).
|
||||
|
||||
---
|
||||
|
||||
## 🚨 Login & Auth "Gotchas"
|
||||
|
||||
### 1. `Invalid Credentials` Right After Fresh Seed
|
||||
If you completely reset the DB using `start.sh` (Option 6) or manually clear Postgres, and your first login returns "Invalid credentials", **do not panic and do not manually change the DB hash**.
|
||||
* **Cause**: The Node.js seeder calculates the bcrypt hash using an environment variable called `PASSWORD_PEPPER` (`gohorse-pepper`). If this variable was empty or mismatched with the Backend API, the hash is physically wrong.
|
||||
* **Fix**: Ensure your `.env` files in both `backend` and `seeder-api` contain `PASSWORD_PEPPER=gohorse-pepper`. Then simply run `cd seeder-api && npm run seed` again to fix the hashes.
|
||||
|
||||
### 2. Login Payload Fields
|
||||
The frontend sends:
|
||||
```json
|
||||
{
|
||||
"email": "lol",
|
||||
"password": "..."
|
||||
}
|
||||
```
|
||||
**Important:** Even though the frontend passes the username `lol`, the JSON key **must** be `"email"`. The Go backend natively handles resolving `"email"` against the `identifier` OR `email` database columns.
|
||||
|
||||
### 3. Account Status Flags
|
||||
The `users` table has an `is_active` boolean. If a user is manually deactivated by an admin, the login endpoint will return a generic `401 Unauthorized` (or specific Account Locked error depending on the exact route version) to prevent user enumeration.
|
||||
|
|
|
|||
Loading…
Reference in a new issue