diff --git a/backend/internal/infrastructure/persistence/postgres/company_repository.go b/backend/internal/infrastructure/persistence/postgres/company_repository.go index d408e20..8937142 100644 --- a/backend/internal/infrastructure/persistence/postgres/company_repository.go +++ b/backend/internal/infrastructure/persistence/postgres/company_repository.go @@ -4,8 +4,6 @@ import ( "context" "database/sql" "errors" - "fmt" - "strconv" "time" "github.com/rede5/gohorsejobs/backend/internal/core/domain/entity" @@ -20,7 +18,7 @@ func NewCompanyRepository(db *sql.DB) *CompanyRepository { } func (r *CompanyRepository) Save(ctx context.Context, company *entity.Company) (*entity.Company, error) { - // companies table uses SERIAL id, we let DB generate it + // companies table uses UUID id, DB generates it query := ` INSERT INTO companies (name, slug, type, document, email, description, verified, active, created_at, updated_at) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) @@ -28,7 +26,7 @@ func (r *CompanyRepository) Save(ctx context.Context, company *entity.Company) ( ` slug := company.Name // TODO: slugify function - var id int + var id string err := r.db.QueryRowContext(ctx, query, company.Name, slug, @@ -45,7 +43,7 @@ func (r *CompanyRepository) Save(ctx context.Context, company *entity.Company) ( if err != nil { return nil, err } - company.ID = strconv.Itoa(id) + company.ID = id return company, nil } @@ -55,54 +53,40 @@ func (r *CompanyRepository) FindByID(ctx context.Context, id string) (*entity.Co created_at, updated_at FROM companies WHERE id = $1` - numID, err := strconv.Atoi(id) - if err != nil { - return nil, fmt.Errorf("invalid company id: %s", id) - } - - row := r.db.QueryRowContext(ctx, query, numID) + row := r.db.QueryRowContext(ctx, query, id) c := &entity.Company{} - var dbID int - err = row.Scan(&dbID, &c.Name, &c.Document, &c.Contact, &c.Status, &c.CreatedAt, &c.UpdatedAt) + var dbID string + err := row.Scan(&dbID, &c.Name, &c.Document, &c.Contact, &c.Status, &c.CreatedAt, &c.UpdatedAt) if err == sql.ErrNoRows { return nil, errors.New("company not found") } - c.ID = strconv.Itoa(dbID) + c.ID = dbID return c, err } func (r *CompanyRepository) Update(ctx context.Context, company *entity.Company) (*entity.Company, error) { company.UpdatedAt = time.Now() - numID, err := strconv.Atoi(company.ID) - if err != nil { - return nil, fmt.Errorf("invalid company id: %s", company.ID) - } - query := ` UPDATE companies SET name=$1, document=$2, email=$3, active=$4, updated_at=$5 WHERE id=$6 ` - _, err = r.db.ExecContext(ctx, query, + _, err := r.db.ExecContext(ctx, query, company.Name, company.Document, company.Contact, company.Status == "ACTIVE", company.UpdatedAt, - numID, + company.ID, ) return company, err } func (r *CompanyRepository) Delete(ctx context.Context, id string) error { - numID, err := strconv.Atoi(id) - if err != nil { - return fmt.Errorf("invalid company id: %s", id) - } query := `DELETE FROM companies WHERE id = $1` - _, err = r.db.ExecContext(ctx, query, numID) + _, err := r.db.ExecContext(ctx, query, id) return err } @@ -120,11 +104,11 @@ func (r *CompanyRepository) FindAll(ctx context.Context) ([]*entity.Company, err var companies []*entity.Company for rows.Next() { c := &entity.Company{} - var dbID int + var dbID string if err := rows.Scan(&dbID, &c.Name, &c.Document, &c.Contact, &c.Status, &c.CreatedAt, &c.UpdatedAt); err != nil { return nil, err } - c.ID = strconv.Itoa(dbID) + c.ID = dbID companies = append(companies, c) } return companies, nil diff --git a/backend/internal/infrastructure/persistence/postgres/user_repository.go b/backend/internal/infrastructure/persistence/postgres/user_repository.go index dd27bcf..7876f31 100644 --- a/backend/internal/infrastructure/persistence/postgres/user_repository.go +++ b/backend/internal/infrastructure/persistence/postgres/user_repository.go @@ -3,8 +3,6 @@ package postgres import ( "context" "database/sql" - "fmt" - "strconv" "time" "github.com/rede5/gohorsejobs/backend/internal/core/domain/entity" @@ -25,23 +23,20 @@ func (r *UserRepository) Save(ctx context.Context, user *entity.User) (*entity.U } defer tx.Rollback() - // Convert tenant_id from string to int - var tenantID *int + // TenantID is string (UUID) or empty + var tenantID *string if user.TenantID != "" { - tid, err := strconv.Atoi(user.TenantID) - if err == nil { - tenantID = &tid - } + tenantID = &user.TenantID } - // 1. Insert User - users table has SERIAL id + // 1. Insert User - users table has UUID id query := ` INSERT INTO users (identifier, password_hash, role, full_name, email, name, tenant_id, status, created_at, updated_at) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) RETURNING id ` - var id int + var id string // Map the first role to the role column, default to 'jobSeeker' role := "jobSeeker" if len(user.Roles) > 0 { @@ -64,7 +59,7 @@ func (r *UserRepository) Save(ctx context.Context, user *entity.User) (*entity.U if err != nil { return nil, err } - user.ID = strconv.Itoa(id) + user.ID = id // 2. Insert Roles into user_roles table if len(user.Roles) > 0 { @@ -91,7 +86,7 @@ func (r *UserRepository) FindByEmail(ctx context.Context, email string) (*entity row := r.db.QueryRowContext(ctx, query, email) u := &entity.User{} - var dbID int + var dbID string err := row.Scan(&dbID, &u.TenantID, &u.Name, &u.Email, &u.PasswordHash, &u.Status, &u.CreatedAt, &u.UpdatedAt) if err != nil { if err == sql.ErrNoRows { @@ -99,42 +94,32 @@ func (r *UserRepository) FindByEmail(ctx context.Context, email string) (*entity } return nil, err } - u.ID = strconv.Itoa(dbID) + u.ID = dbID u.Roles, _ = r.getRoles(ctx, dbID) return u, nil } func (r *UserRepository) FindByID(ctx context.Context, id string) (*entity.User, error) { - numID, err := strconv.Atoi(id) - if err != nil { - return nil, fmt.Errorf("invalid user id: %s", id) - } - query := `SELECT id, COALESCE(tenant_id::text, ''), COALESCE(name, full_name, ''), COALESCE(email, identifier), password_hash, COALESCE(status, 'active'), created_at, updated_at FROM users WHERE id = $1` - row := r.db.QueryRowContext(ctx, query, numID) + row := r.db.QueryRowContext(ctx, query, id) u := &entity.User{} - var dbID int - err = row.Scan(&dbID, &u.TenantID, &u.Name, &u.Email, &u.PasswordHash, &u.Status, &u.CreatedAt, &u.UpdatedAt) + var dbID string + err := row.Scan(&dbID, &u.TenantID, &u.Name, &u.Email, &u.PasswordHash, &u.Status, &u.CreatedAt, &u.UpdatedAt) if err != nil { return nil, err } - u.ID = strconv.Itoa(dbID) + u.ID = dbID u.Roles, _ = r.getRoles(ctx, dbID) return u, nil } func (r *UserRepository) FindAllByTenant(ctx context.Context, tenantID string, limit, offset int) ([]*entity.User, int, error) { - numTenantID, err := strconv.Atoi(tenantID) - if err != nil { - return nil, 0, fmt.Errorf("invalid tenant id: %s", tenantID) - } - var total int countQuery := `SELECT COUNT(*) FROM users WHERE tenant_id = $1` - if err := r.db.QueryRowContext(ctx, countQuery, numTenantID).Scan(&total); err != nil { + if err := r.db.QueryRowContext(ctx, countQuery, tenantID).Scan(&total); err != nil { return nil, 0, err } @@ -144,7 +129,7 @@ func (r *UserRepository) FindAllByTenant(ctx context.Context, tenantID string, l WHERE tenant_id = $1 ORDER BY created_at DESC LIMIT $2 OFFSET $3` - rows, err := r.db.QueryContext(ctx, query, numTenantID, limit, offset) + rows, err := r.db.QueryContext(ctx, query, tenantID, limit, offset) if err != nil { return nil, 0, err } @@ -153,11 +138,11 @@ func (r *UserRepository) FindAllByTenant(ctx context.Context, tenantID string, l var users []*entity.User for rows.Next() { u := &entity.User{} - var dbID int + var dbID string if err := rows.Scan(&dbID, &u.TenantID, &u.Name, &u.Email, &u.PasswordHash, &u.Status, &u.CreatedAt, &u.UpdatedAt); err != nil { return nil, 0, err } - u.ID = strconv.Itoa(dbID) + u.ID = dbID u.Roles, _ = r.getRoles(ctx, dbID) users = append(users, u) } @@ -165,27 +150,18 @@ func (r *UserRepository) FindAllByTenant(ctx context.Context, tenantID string, l } func (r *UserRepository) Update(ctx context.Context, user *entity.User) (*entity.User, error) { - numID, err := strconv.Atoi(user.ID) - if err != nil { - return nil, fmt.Errorf("invalid user id: %s", user.ID) - } - user.UpdatedAt = time.Now() query := `UPDATE users SET name=$1, email=$2, status=$3, updated_at=$4 WHERE id=$5` - _, err = r.db.ExecContext(ctx, query, user.Name, user.Email, user.Status, user.UpdatedAt, numID) + _, err := r.db.ExecContext(ctx, query, user.Name, user.Email, user.Status, user.UpdatedAt, user.ID) return user, err } func (r *UserRepository) Delete(ctx context.Context, id string) error { - numID, err := strconv.Atoi(id) - if err != nil { - return fmt.Errorf("invalid user id: %s", id) - } - _, err = r.db.ExecContext(ctx, `DELETE FROM users WHERE id=$1`, numID) + _, err := r.db.ExecContext(ctx, `DELETE FROM users WHERE id=$1`, id) return err } -func (r *UserRepository) getRoles(ctx context.Context, userID int) ([]entity.Role, error) { +func (r *UserRepository) getRoles(ctx context.Context, userID string) ([]entity.Role, error) { rows, err := r.db.QueryContext(ctx, `SELECT role FROM user_roles WHERE user_id = $1`, userID) if err != nil { return nil, err diff --git a/backend/migrations/009_create_uuid_v7_function.sql b/backend/migrations/000_init_uuid_v7.sql similarity index 100% rename from backend/migrations/009_create_uuid_v7_function.sql rename to backend/migrations/000_init_uuid_v7.sql diff --git a/backend/migrations/001_create_users_table.sql b/backend/migrations/001_create_users_table.sql index 3f2e712..9f26a72 100755 --- a/backend/migrations/001_create_users_table.sql +++ b/backend/migrations/001_create_users_table.sql @@ -2,7 +2,7 @@ -- Description: Stores all system users (SuperAdmin, CompanyAdmin, Recruiter, JobSeeker) CREATE TABLE IF NOT EXISTS users ( - id SERIAL PRIMARY KEY, + id UUID PRIMARY KEY DEFAULT uuid_generate_v7(), identifier VARCHAR(100) UNIQUE NOT NULL, password_hash VARCHAR(255) NOT NULL, role VARCHAR(20) NOT NULL CHECK (role IN ('superadmin', 'companyAdmin', 'recruiter', 'jobSeeker')), diff --git a/backend/migrations/002_create_companies_table.sql b/backend/migrations/002_create_companies_table.sql index 90af7d3..edb735d 100755 --- a/backend/migrations/002_create_companies_table.sql +++ b/backend/migrations/002_create_companies_table.sql @@ -2,7 +2,7 @@ -- Description: Stores company information for employers CREATE TABLE IF NOT EXISTS companies ( - id SERIAL PRIMARY KEY, + id UUID PRIMARY KEY DEFAULT uuid_generate_v7(), name VARCHAR(255) NOT NULL, slug VARCHAR(255) UNIQUE NOT NULL, -- URL friendly name type VARCHAR(50) DEFAULT 'company', -- company, agency, etc diff --git a/backend/migrations/003_create_user_companies_table.sql b/backend/migrations/003_create_user_companies_table.sql index 9ae448b..ddb90d8 100755 --- a/backend/migrations/003_create_user_companies_table.sql +++ b/backend/migrations/003_create_user_companies_table.sql @@ -2,9 +2,9 @@ -- Description: N:M relationship for multi-tenant - users can belong to multiple companies CREATE TABLE IF NOT EXISTS user_companies ( - id SERIAL PRIMARY KEY, - user_id INT NOT NULL, - company_id INT NOT NULL, + id UUID PRIMARY KEY DEFAULT uuid_generate_v7(), + user_id UUID NOT NULL, + company_id UUID NOT NULL, role VARCHAR(20) NOT NULL CHECK (role IN ('companyAdmin', 'recruiter')), permissions JSONB, -- Optional granular permissions diff --git a/backend/migrations/005_create_jobs_table.sql b/backend/migrations/005_create_jobs_table.sql index 4ce62bc..91ac505 100755 --- a/backend/migrations/005_create_jobs_table.sql +++ b/backend/migrations/005_create_jobs_table.sql @@ -2,9 +2,9 @@ -- Description: Job postings created by companies CREATE TABLE IF NOT EXISTS jobs ( - id SERIAL PRIMARY KEY, - company_id INT NOT NULL, - created_by INT NOT NULL, -- user who created the job + id UUID PRIMARY KEY DEFAULT uuid_generate_v7(), + company_id UUID NOT NULL, + created_by UUID NOT NULL, -- user who created the job -- Job Details title VARCHAR(255) NOT NULL, diff --git a/backend/migrations/006_create_applications_table.sql b/backend/migrations/006_create_applications_table.sql index 147f381..df79362 100755 --- a/backend/migrations/006_create_applications_table.sql +++ b/backend/migrations/006_create_applications_table.sql @@ -2,9 +2,9 @@ -- Description: Job applications (can be from registered users or guests) CREATE TABLE IF NOT EXISTS applications ( - id SERIAL PRIMARY KEY, - job_id INT NOT NULL, - user_id INT, -- NULL for guest applications + id UUID PRIMARY KEY DEFAULT uuid_generate_v7(), + job_id UUID NOT NULL, + user_id UUID, -- NULL for guest applications -- Applicant Info (required for guest applications) name VARCHAR(255), diff --git a/backend/migrations/007_create_favorite_jobs_table.sql b/backend/migrations/007_create_favorite_jobs_table.sql index 918f640..ac8ee97 100755 --- a/backend/migrations/007_create_favorite_jobs_table.sql +++ b/backend/migrations/007_create_favorite_jobs_table.sql @@ -3,8 +3,8 @@ CREATE TABLE IF NOT EXISTS favorite_jobs ( id SERIAL PRIMARY KEY, - user_id INT NOT NULL, - job_id INT NOT NULL, + user_id UUID NOT NULL, + job_id UUID NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, diff --git a/backend/migrations/016_create_notifications_table.sql b/backend/migrations/016_create_notifications_table.sql index 5009600..1c64ff8 100644 --- a/backend/migrations/016_create_notifications_table.sql +++ b/backend/migrations/016_create_notifications_table.sql @@ -1,6 +1,6 @@ CREATE TABLE IF NOT EXISTS notifications ( id UUID PRIMARY KEY DEFAULT uuid_generate_v7(), - user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, + user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, type VARCHAR(50) NOT NULL, -- info, success, warning, error title VARCHAR(255) NOT NULL, message TEXT NOT NULL, diff --git a/backend/migrations/017_create_tickets_table.sql b/backend/migrations/017_create_tickets_table.sql index 49ca11e..2f8de9c 100644 --- a/backend/migrations/017_create_tickets_table.sql +++ b/backend/migrations/017_create_tickets_table.sql @@ -3,7 +3,7 @@ DROP TABLE IF EXISTS tickets; CREATE TABLE tickets ( id UUID PRIMARY KEY DEFAULT uuid_generate_v7(), - user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, + user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, subject VARCHAR(255) NOT NULL, status VARCHAR(50) NOT NULL DEFAULT 'open', -- open, in_progress, closed priority VARCHAR(50) NOT NULL DEFAULT 'medium', -- low, medium, high @@ -17,7 +17,7 @@ CREATE INDEX idx_tickets_status ON tickets(status); CREATE TABLE ticket_messages ( id UUID PRIMARY KEY DEFAULT uuid_generate_v7(), ticket_id UUID NOT NULL REFERENCES tickets(id) ON DELETE CASCADE, - user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, -- Sender + user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, -- Sender message TEXT NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); diff --git a/backend/migrations/019_create_job_payments_table.sql b/backend/migrations/019_create_job_payments_table.sql index 883e69b..14a2ff5 100644 --- a/backend/migrations/019_create_job_payments_table.sql +++ b/backend/migrations/019_create_job_payments_table.sql @@ -3,8 +3,8 @@ CREATE TABLE IF NOT EXISTS job_payments ( id UUID PRIMARY KEY DEFAULT uuid_generate_v7(), - job_id INT NOT NULL, - user_id INT, + job_id UUID NOT NULL, + user_id UUID, -- Stripe stripe_session_id VARCHAR(255), @@ -39,7 +39,7 @@ CREATE TABLE IF NOT EXISTS job_payments ( paid_at TIMESTAMP, expires_at TIMESTAMP, - -- Foreign key (INT to match jobs.id SERIAL) + -- Foreign key FOREIGN KEY (job_id) REFERENCES jobs(id) ON DELETE CASCADE, FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE SET NULL ); diff --git a/backend/migrations/020_unify_schema.sql b/backend/migrations/020_unify_schema.sql index 943bcb0..91a9450 100644 --- a/backend/migrations/020_unify_schema.sql +++ b/backend/migrations/020_unify_schema.sql @@ -2,7 +2,7 @@ -- Description: Add fields from core_users to users table for unified architecture -- Add tenant_id (references companies.id) -ALTER TABLE users ADD COLUMN IF NOT EXISTS tenant_id INT REFERENCES companies(id); +ALTER TABLE users ADD COLUMN IF NOT EXISTS tenant_id UUID REFERENCES companies(id); -- Add email if not exists (core_users had this) ALTER TABLE users ADD COLUMN IF NOT EXISTS email VARCHAR(255); @@ -15,7 +15,7 @@ ALTER TABLE users ADD COLUMN IF NOT EXISTS status VARCHAR(20) DEFAULT 'active'; -- Create user_roles table (replaces core_user_roles) CREATE TABLE IF NOT EXISTS user_roles ( - user_id INT NOT NULL REFERENCES users(id) ON DELETE CASCADE, + user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, role VARCHAR(50) NOT NULL, PRIMARY KEY (user_id, role) ); diff --git a/docs/API_SECURITY.md b/docs/API_SECURITY.md new file mode 100644 index 0000000..847c7e0 --- /dev/null +++ b/docs/API_SECURITY.md @@ -0,0 +1,107 @@ +# 🔐 API Security & Access Levels + +This document details the security layers, authentication methods, and role-based access control (RBAC) for the GoHorse Jobs API. Use this guide to verify and test route protection. + +## 🛡️ Authentication Methods + +1. **Bearer Token (JWT)** + * Header: `Authorization: Bearer ` + * Used by: Mobile apps, external integrations, simple API tests. + +2. **HttpOnly Cookie** + * Cookie Name: `jwt` + * Used by: Web Frontend (Next.js), Backoffice. + * Properties: `HttpOnly`, `Secure` (in prod), `SameSite=Lax`. + +## 🚦 Access Levels + +| Level | Description | Middleware | +| :--- | :--- | :--- | +| **Public** | Open to everyone (Guests). No check performed. | None | +| **Authenticated** | Requires a valid JWT (Header or Cookie). | `HeaderAuthGuard` | +| **Role-Restricted** | Requires valid JWT + Specific Role claim. | `HeaderAuthGuard` + `RequireRoles(...)` | + +## 🗺️ Route Permission Matrix + +### 🟢 Public Routes +| Method | Route | Description | Notes | +| :--- | :--- | :--- | :--- | +| `POST` | `/api/v1/auth/login` | User Login | Returns JWT + Cookie | +| `POST` | `/api/v1/auth/register` | Candidate Register | Creates `jobSeeker` user | +| `POST` | `/api/v1/companies` | Company Register | Creates company + `companyAdmin` | +| `GET` | `/api/v1/jobs` | List Jobs | Public search/list | +| `GET` | `/api/v1/jobs/{id}` | Get Job | Public details | +| `GET` | `/docs/*` | Swagger UI | API Documentation | + +### 🟡 Authenticated Routes (Any Logged User) +**Requirement**: Valid JWT. + +| Method | Route | Description | +| :--- | :--- | :--- | +| `GET` | `/api/v1/users/me` | Get Own Profile | +| `PATCH` | `/api/v1/users/{id}` | Update Own Profile (Self-check in handler) | +| `GET` | `/api/v1/notifications` | Get Own Notifications | +| `POST` | `/api/v1/applications` | Apply for Job (Candidate) | +| `POST` | `/api/v1/storage/upload-url` | Get S3 Upload URL | +| `POST` | `/api/v1/storage/download-url` | Get S3 Download URL | +| `DELETE` | `/api/v1/storage/files` | Delete S3 File | + +### 🟠 Recruiter / CompanyAdmin Routes +**Requirement**: Role `companyAdmin` OR `recruiter`. + +| Method | Route | Description | +| :--- | :--- | :--- | +| `POST` | `/api/v1/jobs` | Create Job | +| `PUT` | `/api/v1/jobs/{id}` | Update Job | +| `DELETE` | `/api/v1/jobs/{id}` | Delete Job | +| `GET` | `/api/v1/applications` | List Applications (for own jobs) | +| `PUT` | `/api/v1/applications/{id}/status` | Update Application Status | + +### 🔴 Admin / SuperAdmin Routes (Backoffice) +**Requirement**: Role `superadmin` OR `admin`. + +| Method | Route | Description | Middleware Check | +| :--- | :--- | :--- | :--- | +| `GET` | `/api/v1/users` | List All Users | `adminOnly` | +| `POST` | `/api/v1/users` | Create User (Staff) | `adminOnly` | +| `DELETE` | `/api/v1/users/{id}` | Delete User | `adminOnly` | +| `GET` | `/api/v1/users/roles` | List System Roles | `adminOnly` | +| `GET` | `/api/v1/companies` | List Companies (Full) | `adminOnly` | +| `PATCH` | `/api/v1/companies/{id}/status` | Activate/Ban Company | `adminOnly` | +| `GET` | `/api/v1/jobs/moderation` | Moderate Jobs | `adminOnly` | +| `PATCH` | `/api/v1/jobs/{id}/status` | Approve/Reject Job | `adminOnly` | +| `POST` | `/api/v1/jobs/{id}/duplicate` | Admin Duplicate Job | `adminOnly` | +| `GET` | `/api/v1/tags` | List Tags | `adminOnly` | +| `POST` | `/api/v1/tags` | Create Tag | `adminOnly` | +| `PATCH` | `/api/v1/tags/{id}` | Update Tag | `adminOnly` | +| `GET` | `/api/v1/candidates` | List All Candidates | `adminOnly` | +| `GET` | `/api/v1/audit/logins` | View Audit Logs | `adminOnly` | + +## 🧪 Testing Security + +**1. Test Public Access (Should Succeed)** +```bash +curl http://localhost:8521/api/v1/jobs +``` + +**2. Test Protected Route without Token (Should Fail 401)** +```bash +curl http://localhost:8521/api/v1/users/me +# Expected: 401 Unauthorized +``` + +**3. Test Admin Route as Candidate (Should Fail 403)** +1. Login as Candidate -> Get Token A +2. Call Admin Route: +```bash +curl -H "Authorization: Bearer " http://localhost:8521/api/v1/audit/logins +# Expected: 403 Forbidden +``` + +**4. Test Admin Route as Admin (Should Succeed)** +1. Login as SuperAdmin -> Get Token B +2. Call Admin Route: +```bash +curl -H "Authorization: Bearer " http://localhost:8521/api/v1/audit/logins +# Expected: 200 OK +``` diff --git a/docs/DATABASE.md b/docs/DATABASE.md index 252ca6d..fff252b 100644 --- a/docs/DATABASE.md +++ b/docs/DATABASE.md @@ -125,20 +125,20 @@ erDiagram | Table | ID Type | Description | Migration | |-------|---------|-------------|-----------| -| `users` | SERIAL | System users (candidates, recruiters, admins) | 001 | +| `users` | UUID v7 | System users (candidates, recruiters, admins) | 001/021 | | `user_roles` | Composite | Additional roles per user | 020 | -| `companies` | SERIAL | Employer organizations | 002 | -| `user_companies` | SERIAL | User ↔ Company mapping | 003 | +| `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` | SERIAL | Job postings | 005 | -| `applications` | SERIAL | Job applications | 006 | +| `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 v4 | User notifications | 016 | -| `tickets` | UUID v4 | Support tickets | 017 | -| `ticket_messages` | UUID v4 | Ticket messages | 017 | -| `job_payments` | UUID v4 | Payment records | 019 | +| `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 | @@ -600,8 +600,8 @@ The database uses a **hybrid ID strategy**: | Strategy | Tables | Rationale | |----------|--------|-----------| -| **SERIAL (INT)** | users, companies, jobs, applications, regions, cities | Legacy tables, simpler, faster, auto-increment | -| **UUID v7** | notifications, tickets, job_payments | Time-ordered, distributed-friendly, sortable | +| **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) diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index c784e8e..cb38239 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -33,6 +33,8 @@ Roadmap de desenvolvimento do projeto GoHorse Jobs. - [x] Multi-tenancy básico - [x] PASSWORD_PEPPER para hash seguro - [x] 🆕 Schema unificado (eliminado core_*) +- [x] 🆕 Migração UUID v7 (Tabelas Core) +- [x] 🆕 Seeder atualizado (UUID compatible) ### Frontend - [x] Login/Logout com cookies @@ -53,7 +55,7 @@ Roadmap de desenvolvimento do projeto GoHorse Jobs. - [x] Regiões (BR, US, JP) - [x] Cidades - [x] Notificações -- [x] 🆕 Schema unificado +- [x] 🆕 Schema unificado e UUID ### DevOps - [x] Docker setup backend @@ -94,8 +96,8 @@ Roadmap de desenvolvimento do projeto GoHorse Jobs. ### Alta Prioridade - [ ] Email transacional (welcome, reset, application) -- [ ] Integração Stripe completa -- [ ] Busca avançada com filtros +- [ ] Integração Stripe completa (Webhook handlers) +- [ ] Busca avançada com filtros (Backend pronto, Frontend pendente) - [ ] Internacionalização (i18n) - [ ] Testes E2E frontend