diff --git a/backend/internal/infrastructure/persistence/postgres/company_repository.go b/backend/internal/infrastructure/persistence/postgres/company_repository.go index 101fdd9..d408e20 100644 --- a/backend/internal/infrastructure/persistence/postgres/company_repository.go +++ b/backend/internal/infrastructure/persistence/postgres/company_repository.go @@ -4,9 +4,10 @@ import ( "context" "database/sql" "errors" + "fmt" + "strconv" "time" - "github.com/google/uuid" "github.com/rede5/gohorsejobs/backend/internal/core/domain/entity" ) @@ -19,70 +20,97 @@ func NewCompanyRepository(db *sql.DB) *CompanyRepository { } func (r *CompanyRepository) Save(ctx context.Context, company *entity.Company) (*entity.Company, error) { - if company.ID == "" { - company.ID = uuid.New().String() - } - + // companies table uses SERIAL id, we let DB generate it query := ` - INSERT INTO core_companies (id, name, document, contact, status, created_at, updated_at) - VALUES ($1, $2, $3, $4, $5, $6, $7) + 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) RETURNING id ` + slug := company.Name // TODO: slugify function + var id int err := r.db.QueryRowContext(ctx, query, - company.ID, company.Name, + slug, + "company", company.Document, - company.Contact, - company.Status, + company.Contact, // mapped to email + "{}", // description as JSON + true, // verified + company.Status == "ACTIVE", company.CreatedAt, company.UpdatedAt, - ).Scan(&company.ID) + ).Scan(&id) if err != nil { return nil, err } + company.ID = strconv.Itoa(id) return company, nil } func (r *CompanyRepository) FindByID(ctx context.Context, id string) (*entity.Company, error) { - query := `SELECT id, name, document, contact, status, created_at, updated_at FROM core_companies WHERE id = $1` - row := r.db.QueryRowContext(ctx, query, id) + query := `SELECT id, name, document, email, + CASE WHEN active THEN 'ACTIVE' ELSE 'INACTIVE' END as status, + 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) c := &entity.Company{} - err := row.Scan(&c.ID, &c.Name, &c.Document, &c.Contact, &c.Status, &c.CreatedAt, &c.UpdatedAt) + var dbID int + 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) 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 core_companies - SET name=$1, document=$2, contact=$3, status=$4, updated_at=$5 + 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, + company.Status == "ACTIVE", company.UpdatedAt, - company.ID, + numID, ) return company, err } func (r *CompanyRepository) Delete(ctx context.Context, id string) error { - query := `DELETE FROM core_companies WHERE id = $1` - _, err := r.db.ExecContext(ctx, query, id) + 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) return err } func (r *CompanyRepository) FindAll(ctx context.Context) ([]*entity.Company, error) { - query := `SELECT id, name, document, contact, status, created_at, updated_at FROM core_companies` + query := `SELECT id, name, document, email, + CASE WHEN active THEN 'ACTIVE' ELSE 'INACTIVE' END as status, + created_at, updated_at + FROM companies` rows, err := r.db.QueryContext(ctx, query) if err != nil { return nil, err @@ -92,9 +120,11 @@ func (r *CompanyRepository) FindAll(ctx context.Context) ([]*entity.Company, err var companies []*entity.Company for rows.Next() { c := &entity.Company{} - if err := rows.Scan(&c.ID, &c.Name, &c.Document, &c.Contact, &c.Status, &c.CreatedAt, &c.UpdatedAt); err != nil { + var dbID int + 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) 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 704bc19..dd27bcf 100644 --- a/backend/internal/infrastructure/persistence/postgres/user_repository.go +++ b/backend/internal/infrastructure/persistence/postgres/user_repository.go @@ -3,10 +3,10 @@ package postgres import ( "context" "database/sql" - "encoding/json" + "fmt" + "strconv" "time" - "github.com/google/uuid" "github.com/rede5/gohorsejobs/backend/internal/core/domain/entity" ) @@ -19,47 +19,58 @@ func NewUserRepository(db *sql.DB) *UserRepository { } func (r *UserRepository) Save(ctx context.Context, user *entity.User) (*entity.User, error) { - if user.ID == "" { - user.ID = uuid.New().String() - } - tx, err := r.db.BeginTx(ctx, nil) if err != nil { return nil, err } defer tx.Rollback() - // Serialize metadata - metadata, err := json.Marshal(user.Metadata) - if err != nil { - return nil, err + // Convert tenant_id from string to int + var tenantID *int + if user.TenantID != "" { + tid, err := strconv.Atoi(user.TenantID) + if err == nil { + tenantID = &tid + } } - // 1. Insert User + // 1. Insert User - users table has SERIAL id query := ` - INSERT INTO core_users (id, tenant_id, name, email, password_hash, status, metadata, created_at, updated_at) - VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9) + 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 ` - _, err = tx.ExecContext(ctx, query, - user.ID, - user.TenantID, + + var id int + // Map the first role to the role column, default to 'jobSeeker' + role := "jobSeeker" + if len(user.Roles) > 0 { + role = user.Roles[0].Name + } + + err = tx.QueryRowContext(ctx, query, + user.Email, // identifier = email for now + user.PasswordHash, + role, user.Name, user.Email, - user.PasswordHash, + user.Name, + tenantID, user.Status, - metadata, user.CreatedAt, user.UpdatedAt, - ) + ).Scan(&id) + if err != nil { return nil, err } + user.ID = strconv.Itoa(id) - // 2. Insert Roles + // 2. Insert Roles into user_roles table if len(user.Roles) > 0 { - roleQuery := `INSERT INTO core_user_roles (user_id, role) VALUES ($1, $2)` + roleQuery := `INSERT INTO user_roles (user_id, role) VALUES ($1, $2) ON CONFLICT DO NOTHING` for _, role := range user.Roles { - _, err := tx.ExecContext(ctx, roleQuery, user.ID, role.Name) + _, err := tx.ExecContext(ctx, roleQuery, id, role.Name) if err != nil { return nil, err } @@ -74,49 +85,66 @@ func (r *UserRepository) Save(ctx context.Context, user *entity.User) (*entity.U } func (r *UserRepository) FindByEmail(ctx context.Context, email string) (*entity.User, error) { - query := `SELECT id, tenant_id, name, email, password_hash, status, created_at, updated_at FROM core_users WHERE email = $1` + 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 email = $1 OR identifier = $1` row := r.db.QueryRowContext(ctx, query, email) u := &entity.User{} - err := row.Scan(&u.ID, &u.TenantID, &u.Name, &u.Email, &u.PasswordHash, &u.Status, &u.CreatedAt, &u.UpdatedAt) + var dbID int + 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 { return nil, nil // Return nil if not found } return nil, err } - - u.Roles, _ = r.getRoles(ctx, u.ID) + u.ID = strconv.Itoa(dbID) + u.Roles, _ = r.getRoles(ctx, dbID) return u, nil } func (r *UserRepository) FindByID(ctx context.Context, id string) (*entity.User, error) { - query := `SELECT id, tenant_id, name, email, password_hash, status, created_at, updated_at FROM core_users WHERE id = $1` - row := r.db.QueryRowContext(ctx, query, id) + 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) u := &entity.User{} - err := row.Scan(&u.ID, &u.TenantID, &u.Name, &u.Email, &u.PasswordHash, &u.Status, &u.CreatedAt, &u.UpdatedAt) + var dbID int + 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.Roles, _ = r.getRoles(ctx, u.ID) + u.ID = strconv.Itoa(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 core_users WHERE tenant_id = $1` - if err := r.db.QueryRowContext(ctx, countQuery, tenantID).Scan(&total); err != nil { + countQuery := `SELECT COUNT(*) FROM users WHERE tenant_id = $1` + if err := r.db.QueryRowContext(ctx, countQuery, numTenantID).Scan(&total); err != nil { return nil, 0, err } - query := `SELECT id, tenant_id, name, email, password_hash, status, created_at, updated_at - FROM core_users + 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 tenant_id = $1 ORDER BY created_at DESC LIMIT $2 OFFSET $3` - rows, err := r.db.QueryContext(ctx, query, tenantID, limit, offset) + rows, err := r.db.QueryContext(ctx, query, numTenantID, limit, offset) if err != nil { return nil, 0, err } @@ -125,31 +153,40 @@ func (r *UserRepository) FindAllByTenant(ctx context.Context, tenantID string, l var users []*entity.User for rows.Next() { u := &entity.User{} - if err := rows.Scan(&u.ID, &u.TenantID, &u.Name, &u.Email, &u.PasswordHash, &u.Status, &u.CreatedAt, &u.UpdatedAt); err != nil { + var dbID int + 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 } - // Populate roles N+1? Ideally join, but for now simple - u.Roles, _ = r.getRoles(ctx, u.ID) + u.ID = strconv.Itoa(dbID) + u.Roles, _ = r.getRoles(ctx, dbID) users = append(users, u) } return users, total, nil } func (r *UserRepository) Update(ctx context.Context, user *entity.User) (*entity.User, error) { - // Not fully implemented for roles update for brevity, just fields + 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 core_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, user.ID) + 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) return user, err } func (r *UserRepository) Delete(ctx context.Context, id string) error { - _, err := r.db.ExecContext(ctx, `DELETE FROM core_users WHERE id=$1`, id) + 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) return err } -func (r *UserRepository) getRoles(ctx context.Context, userID string) ([]entity.Role, error) { - rows, err := r.db.QueryContext(ctx, `SELECT role FROM core_user_roles WHERE user_id = $1`, userID) +func (r *UserRepository) getRoles(ctx context.Context, userID int) ([]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_core_tables.sql b/backend/migrations/009_create_core_tables.sql.disabled similarity index 100% rename from backend/migrations/009_create_core_tables.sql rename to backend/migrations/009_create_core_tables.sql.disabled diff --git a/backend/migrations/010_seed_super_admin.sql b/backend/migrations/010_seed_super_admin.sql index 8bbc14e..638abf6 100644 --- a/backend/migrations/010_seed_super_admin.sql +++ b/backend/migrations/010_seed_super_admin.sql @@ -1,38 +1,39 @@ --- Migration: Create Super Admin and System Tenant --- Description: Inserts the default System Tenant and Super Admin user. --- Use fixed UUIDs for reproducibility in dev environments. +-- Migration: Create Super Admin and System Company +-- Description: Inserts the default System Company and Super Admin user. +-- Uses unified tables (companies, users, user_roles) --- 1. Insert System Tenant -INSERT INTO core_companies (id, name, document, contact, status, created_at, updated_at) +-- 1. Insert System Company (for SuperAdmin context) +INSERT INTO companies (name, slug, type, document, email, description, verified, active) VALUES ( - '00000000-0000-0000-0000-000000000001', -- Fixed System Tenant ID - 'System Tenant (SuperAdmin Context)', - 'SYSTEM', - 'admin@system.local', - 'ACTIVE', - CURRENT_TIMESTAMP, - CURRENT_TIMESTAMP -) ON CONFLICT (id) DO NOTHING; + 'GoHorse System', + 'gohorse-system', + 'system', + '00.000.000/0001-91', + 'admin@gohorsejobs.com', + '{"tagline": "System Administration Tenant"}', + true, + true +) ON CONFLICT (slug) DO NOTHING; -- 2. Insert Super Admin User -- WARNING: This hash is generated WITHOUT PASSWORD_PEPPER. -- For development only. Use seeder-api for proper user creation. --- Password: "password123" (BCrypt hash without pepper) -INSERT INTO core_users (id, tenant_id, name, email, password_hash, status, created_at, updated_at) -VALUES ( - '00000000-0000-0000-0000-000000000002', -- Fixed SuperAdmin User ID - '00000000-0000-0000-0000-000000000001', -- Link to System Tenant +-- Password: "Admin@2025!" (should be created by seeder with proper pepper) +INSERT INTO users (identifier, password_hash, role, full_name, email, name, status, tenant_id) +SELECT + 'superadmin', + '$2a$10$UWrE9xN39lVagJHlXZsxwOVI3NRSEd1VJ6UzMblW6LOxNmsOZtj9K', -- placeholder, seeder will update + 'superadmin', 'Super Administrator', - 'admin@todai.jobs', - '$2a$10$UWrE9xN39lVagJHlXZsxwOVI3NRSEd1VJ6UzMblW6LOxNmsOZtj9K', -- "password123" - 'ACTIVE', - CURRENT_TIMESTAMP, - CURRENT_TIMESTAMP -) ON CONFLICT (id) DO NOTHING; + 'admin@gohorsejobs.com', + 'Super Administrator', + 'active', + c.id +FROM companies c WHERE c.slug = 'gohorse-system' +ON CONFLICT (identifier) DO NOTHING; -- 3. Assign Super Admin Role -INSERT INTO core_user_roles (user_id, role) -VALUES ( - '00000000-0000-0000-0000-000000000002', - 'SUPER_ADMIN' -) ON CONFLICT (user_id, role) DO NOTHING; +INSERT INTO user_roles (user_id, role) +SELECT u.id, 'SUPER_ADMIN' +FROM users u WHERE u.identifier = 'superadmin' +ON CONFLICT (user_id, role) DO NOTHING; diff --git a/backend/migrations/020_unify_schema.sql b/backend/migrations/020_unify_schema.sql new file mode 100644 index 0000000..943bcb0 --- /dev/null +++ b/backend/migrations/020_unify_schema.sql @@ -0,0 +1,33 @@ +-- Migration: Unify schema - add missing fields to users table +-- 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); + +-- Add email if not exists (core_users had this) +ALTER TABLE users ADD COLUMN IF NOT EXISTS email VARCHAR(255); + +-- Add name if not exists (mapped from full_name or separate) +ALTER TABLE users ADD COLUMN IF NOT EXISTS name VARCHAR(255); + +-- Add status field for compatibility +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, + role VARCHAR(50) NOT NULL, + PRIMARY KEY (user_id, role) +); + +-- Index for user_roles +CREATE INDEX IF NOT EXISTS idx_user_roles_user_id ON user_roles(user_id); +CREATE INDEX IF NOT EXISTS idx_user_roles_role ON user_roles(role); + +-- Index for tenant lookup +CREATE INDEX IF NOT EXISTS idx_users_tenant_id ON users(tenant_id); +CREATE INDEX IF NOT EXISTS idx_users_email ON users(email); + +-- Comments +COMMENT ON COLUMN users.tenant_id IS 'Company ID this user belongs to (NULL for superadmin)'; +COMMENT ON TABLE user_roles IS 'Additional roles for users (e.g., admin, recruiter)'; diff --git a/seeder-api/src/seeders/acme.js b/seeder-api/src/seeders/acme.js index 7e89554..cf511db 100644 --- a/seeder-api/src/seeders/acme.js +++ b/seeder-api/src/seeders/acme.js @@ -139,21 +139,14 @@ export async function seedAcmeCorp() { 'https://upload.wikimedia.org/wikipedia/commons/thumb/6/6f/Wile_E._Coyote_with_Acme_Rocket.svg/200px-Wile_E._Coyote_with_Acme_Rocket.svg.png' ]); - // Create Core Company entry - const acmeCoreId = 'acacacac-acac-acac-acac-acacacacacac'; // Memorable UUID - await pool.query(` - INSERT INTO core_companies (id, name, document, status) - VALUES ($1, $2, $3, 'ACTIVE') - ON CONFLICT (id) DO NOTHING - `, [acmeCoreId, 'ACME Corporation', acmeCNPJ]); - console.log(' ✓ ACME Corporation criada'); - // 2. Get ACME company ID (use coreId for FK) - const acmeCompanyId = acmeCoreId; + // 2. Get ACME company ID from companies table + const acmeRes = await pool.query("SELECT id FROM companies WHERE slug = 'ACME Corporation'"); + const acmeCompanyId = acmeRes.rows[0]?.id; - // 3. Get seed user from core_users (for FK) - const seedUserRes = await pool.query("SELECT id FROM core_users LIMIT 1"); + // 3. Get seed user from users table (for FK) + const seedUserRes = await pool.query("SELECT id FROM users LIMIT 1"); const seedUserId = seedUserRes.rows[0]?.id; // 4. Seed 69 jobs diff --git a/seeder-api/src/seeders/companies.js b/seeder-api/src/seeders/companies.js index 73a8f25..e5d7c0a 100644 --- a/seeder-api/src/seeders/companies.js +++ b/seeder-api/src/seeders/companies.js @@ -87,24 +87,16 @@ export async function seedCompanies() { true, true ]); - - // Seed Core Company (One-to-one mapping) - const coreId = crypto.randomUUID(); - await pool.query(` - INSERT INTO core_companies (id, name, document, status) - VALUES ($1, $2, $3, 'ACTIVE') - ON CONFLICT (id) DO NOTHING - `, [coreId, company.name, generateCNPJ(i)]); } - // Seed System Tenant for SuperAdmin + // Seed System Company for SuperAdmin await pool.query(` - INSERT INTO core_companies (id, name, document, status) - VALUES ('00000000-0000-0000-0000-000000000000', 'GoHorse System', '00.000.000/0001-91', 'ACTIVE') - ON CONFLICT (id) DO NOTHING + INSERT INTO companies (name, slug, type, document, email, description, verified, active) + VALUES ('GoHorse System', 'gohorse-system', 'system', '00.000.000/0001-91', 'admin@gohorsejobs.com', '{"tagline": "System Administration"}', true, true) + ON CONFLICT (slug) DO NOTHING `); - console.log(` ✓ ${companyData.length} companies seeded (Legacy & Core)`); + console.log(` ✓ ${companyData.length + 1} companies seeded`); } catch (error) { console.error(' ❌ Error seeding companies:', error.message); throw error; diff --git a/seeder-api/src/seeders/epic-companies.js b/seeder-api/src/seeders/epic-companies.js index 6ec9cb2..a0739ed 100644 --- a/seeder-api/src/seeders/epic-companies.js +++ b/seeder-api/src/seeders/epic-companies.js @@ -263,7 +263,7 @@ async function createCompanyAndJobs(companyData, jobs) { const regionsRes = await pool.query('SELECT id FROM regions LIMIT 1'); const defaultRegionId = regionsRes.rows[0]?.id || null; - // Create Company + // Create Company (companies uses SERIAL id) await pool.query(` INSERT INTO companies (name, slug, type, document, address, region_id, phone, email, website, description, verified, active) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12) @@ -283,17 +283,11 @@ async function createCompanyAndJobs(companyData, jobs) { true ]); - // Core Company - await pool.query(` - INSERT INTO core_companies (id, name, document, status) - VALUES ($1, $2, $3, 'ACTIVE') - ON CONFLICT (id) DO NOTHING - `, [companyData.coreId, companyData.name, companyData.cnpj]); + // Get company ID from companies table (SERIAL id) + const companyRes = await pool.query("SELECT id FROM companies WHERE slug = $1", [companyData.slug]); + const companyId = companyRes.rows[0]?.id; - // Get company ID (use coreId for FK reference) - const companyId = companyData.coreId; - - const seedUserRes = await pool.query("SELECT id FROM core_users LIMIT 1"); + const seedUserRes = await pool.query("SELECT id FROM users LIMIT 1"); const seedUserId = seedUserRes.rows[0]?.id || 1; const workModes = ['onsite', 'hybrid', 'remote']; diff --git a/seeder-api/src/seeders/fictional-companies.js b/seeder-api/src/seeders/fictional-companies.js index 5fc722b..7bded85 100644 --- a/seeder-api/src/seeders/fictional-companies.js +++ b/seeder-api/src/seeders/fictional-companies.js @@ -177,18 +177,12 @@ export async function seedStarkIndustries() { true ]); - // Core Company - await pool.query(` - INSERT INTO core_companies (id, name, document, status) - VALUES ($1, $2, $3, 'ACTIVE') - ON CONFLICT (id) DO NOTHING - `, ['57575757-5757-5757-5757-575757575757', 'Stark Industries', '77.777.777/0001-77']); - - // Get company ID (use coreId directly) - const companyId = '57575757-5757-5757-5757-575757575757'; + // Get company ID (SERIAL) from companies table + const companyRes = await pool.query("SELECT id FROM companies WHERE slug = 'Stark Industries'"); + const companyId = companyRes.rows[0]?.id; // Get seed user - const seedUserRes = await pool.query("SELECT id FROM core_users LIMIT 1"); + const seedUserRes = await pool.query("SELECT id FROM users LIMIT 1"); const seedUserId = seedUserRes.rows[0]?.id || 1; // Create jobs @@ -265,7 +259,7 @@ export async function seedLosPollosHermanos() { // Core Company await pool.query(` - INSERT INTO core_companies (id, name, document, status) + INSERT INTO companies (id, name, document, status) VALUES ($1, $2, $3, 'ACTIVE') ON CONFLICT (id) DO NOTHING `, ['66666666-6666-6666-6666-666666666666', 'Los Pollos Hermanos', '66.666.666/0001-66']); @@ -274,7 +268,7 @@ export async function seedLosPollosHermanos() { // Get company ID (use coreId directly) const companyId = '66666666-6666-6666-6666-666666666666'; - const seedUserRes = await pool.query("SELECT id FROM core_users LIMIT 1"); + const seedUserRes = await pool.query("SELECT id FROM users LIMIT 1"); const seedUserId = seedUserRes.rows[0]?.id || 1; const benefits = ['Frango Grátis (apenas o legal)', 'Plano de Saúde Premium', 'Seguro de Vida Reforçado', 'Carro da Empresa (GPS desativado)']; @@ -352,7 +346,7 @@ export async function seedSpringfieldNuclear() { // Core Company await pool.query(` - INSERT INTO core_companies (id, name, document, status) + INSERT INTO companies (id, name, document, status) VALUES ($1, $2, $3, 'ACTIVE') ON CONFLICT (id) DO NOTHING `, ['88888888-8888-8888-8888-888888888888', 'Springfield Nuclear Power Plant', '88.888.888/0001-88']); @@ -361,7 +355,7 @@ export async function seedSpringfieldNuclear() { // Get company ID (use coreId directly) const companyId = '88888888-8888-8888-8888-888888888888'; - const seedUserRes = await pool.query("SELECT id FROM core_users LIMIT 1"); + const seedUserRes = await pool.query("SELECT id FROM users LIMIT 1"); const seedUserId = seedUserRes.rows[0]?.id || 1; const benefits = ['Tickets de Cafeteria', 'Dosímetro Pessoal', 'Desconto na Taverna do Moe', 'Estacionamento (longe do reator)']; diff --git a/seeder-api/src/seeders/notifications.js b/seeder-api/src/seeders/notifications.js index b2b2a38..5e5693a 100644 --- a/seeder-api/src/seeders/notifications.js +++ b/seeder-api/src/seeders/notifications.js @@ -78,8 +78,8 @@ export async function seedNotifications() { console.log('🔔 Seeding notifications...'); try { - // Get users from core_users - const usersRes = await pool.query('SELECT id FROM core_users LIMIT 5'); + // Get users from users + const usersRes = await pool.query('SELECT id FROM users LIMIT 5'); const users = usersRes.rows; if (users.length === 0) { diff --git a/seeder-api/src/seeders/users.js b/seeder-api/src/seeders/users.js index 7f63f87..901ea15 100644 --- a/seeder-api/src/seeders/users.js +++ b/seeder-api/src/seeders/users.js @@ -1,110 +1,105 @@ import bcrypt from 'bcrypt'; -import crypto from 'crypto'; import { pool } from '../db.js'; // Get pepper from environment - MUST match backend PASSWORD_PEPPER const PASSWORD_PEPPER = process.env.PASSWORD_PEPPER || ''; export async function seedUsers() { - console.log('👤 Seeding users (Core Architecture)...'); + console.log('👤 Seeding users (Unified Architecture)...'); try { - // Fetch core companies to map users - const companiesResult = await pool.query('SELECT id, name FROM core_companies'); + // Fetch companies to map users (now using companies table, not core_companies) + const companiesResult = await pool.query('SELECT id, name, slug FROM companies'); const companyMap = {}; // name -> id companiesResult.rows.forEach(c => companyMap[c.name] = c.id); - const systemTenantId = '00000000-0000-0000-0000-000000000000'; + + // Get system tenant ID + const systemResult = await pool.query("SELECT id FROM companies WHERE slug = 'gohorse-system'"); + const systemTenantId = systemResult.rows[0]?.id || null; // 1. Create SuperAdmin (Requested: superadmin / Admin@2025!) const superAdminPassword = await bcrypt.hash('Admin@2025!' + PASSWORD_PEPPER, 10); - const superAdminId = crypto.randomUUID(); - // User requested identifier 'superadmin' - const superAdminIdentifier = 'superadmin'; const result = await pool.query(` - INSERT INTO core_users (id, tenant_id, name, email, password_hash, status) - VALUES ($1, $2, $3, $4, $5, 'ACTIVE') - ON CONFLICT (tenant_id, email) DO UPDATE SET password_hash = EXCLUDED.password_hash + INSERT INTO users (identifier, password_hash, role, full_name, email, name, tenant_id, status) + VALUES ($1, $2, 'superadmin', $3, $4, $5, $6, 'active') + ON CONFLICT (identifier) DO UPDATE SET password_hash = EXCLUDED.password_hash RETURNING id - `, [superAdminId, systemTenantId, 'System Administrator', superAdminIdentifier, superAdminPassword]); + `, ['superadmin', superAdminPassword, 'System Administrator', 'admin@gohorsejobs.com', 'System Administrator', systemTenantId]); - const actualSuperAdminId = result.rows[0].id; + const superAdminId = result.rows[0].id; - // Role + // Role in user_roles table await pool.query(` - INSERT INTO core_user_roles (user_id, role) + INSERT INTO user_roles (user_id, role) VALUES ($1, 'superadmin') ON CONFLICT (user_id, role) DO NOTHING - `, [actualSuperAdminId]); + `, [superAdminId]); console.log(' ✓ SuperAdmin created (superadmin)'); // 2. Create Company Admins const companyAdmins = [ - // Requested: takeshi_yamamoto / Takeshi@2025 (Company Admin) - { identifier: 'takeshi_yamamoto', fullName: 'Takeshi Yamamoto', company: 'TechCorp', email: 'takeshi_yamamoto', pass: 'Takeshi@2025', roles: ['admin', 'company'] }, + { identifier: 'takeshi_yamamoto', fullName: 'Takeshi Yamamoto', company: 'TechCorp', email: 'takeshi@techcorp.com', pass: 'Takeshi@2025', roles: ['admin', 'company'] }, { identifier: 'kenji', fullName: 'Kenji Tanaka', company: 'AppMakers', email: 'kenji@appmakers.mobile', pass: 'Takeshi@2025', roles: ['admin', 'company'] }, - // Requested: maria_santos / User@2025 (Recruiter) - Placing in DesignHub - { identifier: 'maria_santos', fullName: 'Maria Santos', company: 'DesignHub', email: 'maria_santos', pass: 'User@2025', roles: ['recruiter', 'company'] } + { identifier: 'maria_santos', fullName: 'Maria Santos', company: 'DesignHub', email: 'maria@designhub.com', pass: 'User@2025', roles: ['recruiter', 'company'] } ]; for (const admin of companyAdmins) { const hash = await bcrypt.hash(admin.pass + PASSWORD_PEPPER, 10); - const userId = crypto.randomUUID(); const tenantId = companyMap[admin.company]; if (!tenantId) { - console.warn(`Original company ${admin.company} not found for user ${admin.fullName}, skipping.`); + console.warn(` ⚠️ Company ${admin.company} not found for user ${admin.fullName}, skipping.`); continue; } const result = await pool.query(` - INSERT INTO core_users (id, tenant_id, name, email, password_hash, status) - VALUES ($1, $2, $3, $4, $5, 'ACTIVE') - ON CONFLICT (tenant_id, email) DO UPDATE SET password_hash = EXCLUDED.password_hash + INSERT INTO users (identifier, password_hash, role, full_name, email, name, tenant_id, status) + VALUES ($1, $2, $3, $4, $5, $6, $7, 'active') + ON CONFLICT (identifier) DO UPDATE SET password_hash = EXCLUDED.password_hash RETURNING id - `, [userId, tenantId, admin.fullName, admin.email, hash]); + `, [admin.identifier, hash, admin.roles[0], admin.fullName, admin.email, admin.fullName, tenantId]); - const actualUserId = result.rows[0].id; + const userId = result.rows[0].id; for (const role of admin.roles) { await pool.query(` - INSERT INTO core_user_roles (user_id, role) + INSERT INTO user_roles (user_id, role) VALUES ($1, $2) ON CONFLICT (user_id, role) DO NOTHING - `, [actualUserId, role]); + `, [userId, role]); } - console.log(` ✓ User created: ${admin.email}`); + console.log(` ✓ User created: ${admin.identifier}`); } // 3. Create Candidates (Job Seekers) - // Requested: paulo_santos / User@2025 const candidates = [ - { fullName: 'Paulo Santos', email: 'paulo_santos', pass: 'User@2025' }, - { fullName: 'Maria Reyes', email: 'maria@email.com', pass: 'User@2025' } + { identifier: 'paulo_santos', fullName: 'Paulo Santos', email: 'paulo@email.com', pass: 'User@2025' }, + { identifier: 'maria_email', fullName: 'Maria Reyes', email: 'maria@email.com', pass: 'User@2025' } ]; for (const cand of candidates) { const hash = await bcrypt.hash(cand.pass + PASSWORD_PEPPER, 10); - const userId = crypto.randomUUID(); const result = await pool.query(` - INSERT INTO core_users (id, tenant_id, name, email, password_hash, status) - VALUES ($1, $2, $3, $4, $5, 'ACTIVE') - ON CONFLICT (tenant_id, email) DO UPDATE SET password_hash = EXCLUDED.password_hash + INSERT INTO users (identifier, password_hash, role, full_name, email, name, status) + VALUES ($1, $2, 'jobSeeker', $3, $4, $5, 'active') + ON CONFLICT (identifier) DO UPDATE SET password_hash = EXCLUDED.password_hash RETURNING id - `, [userId, systemTenantId, cand.fullName, cand.email, hash]); + `, [cand.identifier, hash, cand.fullName, cand.email, cand.fullName]); - const actualUserId = result.rows[0].id; + const userId = result.rows[0].id; await pool.query(` - INSERT INTO core_user_roles (user_id, role) + INSERT INTO user_roles (user_id, role) VALUES ($1, 'candidate') ON CONFLICT (user_id, role) DO NOTHING - `, [actualUserId]); - console.log(` ✓ Candidate created: ${cand.email}`); + `, [userId]); + + console.log(` ✓ Candidate created: ${cand.identifier}`); } console.log('👤 Seeding legacy candidates...'); @@ -120,7 +115,7 @@ export async function seedUsers() { title: 'Full Stack Developer', experience: '5 years of experience', skills: ['React', 'Node.js', 'TypeScript', 'AWS', 'Docker'], - bio: 'Developer passionate about building innovative solutions. Experience in React, Node.js, and cloud computing.', + bio: 'Developer passionate about building innovative solutions.', objective: 'Grow as a full stack developer building scalable products.' }, { @@ -133,7 +128,7 @@ export async function seedUsers() { title: 'UX/UI Designer', experience: '3 years of experience', skills: ['Figma', 'Adobe XD', 'UI Design', 'Prototyping', 'Design Systems'], - bio: 'Designer focused on creating memorable experiences. Specialist in design systems and prototyping.', + bio: 'Designer focused on creating memorable experiences.', objective: 'Design intuitive experiences for web and mobile products.' }, { @@ -146,7 +141,7 @@ export async function seedUsers() { title: 'Data Engineer', experience: '7 years of experience', skills: ['Python', 'SQL', 'Spark', 'Machine Learning', 'Data Visualization'], - bio: 'Data engineer with a strong background in machine learning and big data. Passionate about turning data into insights.', + bio: 'Data engineer with a strong background in machine learning.', objective: 'Build robust data pipelines and analytics products.' }, { @@ -159,7 +154,7 @@ export async function seedUsers() { title: 'Product Manager', experience: '6 years of experience', skills: ['Product Management', 'Agile', 'Scrum', 'Data Analysis', 'User Research'], - bio: 'Product Manager with experience in digital products and agile methodologies. Focused on delivering user value.', + bio: 'Product Manager with experience in digital products.', objective: 'Lead cross-functional teams to deliver customer-centric products.' }, { @@ -172,7 +167,7 @@ export async function seedUsers() { title: 'DevOps Engineer', experience: '4 years of experience', skills: ['Docker', 'Kubernetes', 'AWS', 'Terraform', 'CI/CD'], - bio: 'DevOps engineer specialized in automation and cloud infrastructure. Experience with Kubernetes and CI/CD.', + bio: 'DevOps engineer specialized in automation and cloud infrastructure.', objective: 'Improve delivery pipelines and cloud reliability.' } ]; @@ -181,20 +176,9 @@ export async function seedUsers() { const hash = await bcrypt.hash('User@2025' + PASSWORD_PEPPER, 10); await pool.query(` INSERT INTO users ( - identifier, - password_hash, - role, - full_name, - email, - phone, - city, - state, - title, - experience, - skills, - objective, - bio - ) VALUES ($1, $2, 'jobSeeker', $3, $4, $5, $6, $7, $8, $9, $10, $11, $12) + identifier, password_hash, role, full_name, email, name, phone, + city, state, title, experience, skills, objective, bio, status + ) VALUES ($1, $2, 'jobSeeker', $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, 'active') ON CONFLICT (identifier) DO UPDATE SET full_name = EXCLUDED.full_name, email = EXCLUDED.email, @@ -211,6 +195,7 @@ export async function seedUsers() { hash, cand.fullName, cand.email, + cand.fullName, cand.phone, cand.city, cand.state,