refactor: unify schema - eliminate core_* tables

BREAKING CHANGE: Removed core_companies, core_users, core_user_roles tables

Migrations:
- Create 020_unify_schema.sql: adds tenant_id, email, name to users table
- Create user_roles table (replaces core_user_roles)
- Disable 009_create_core_tables.sql (renamed to .disabled)
- Update 010_seed_super_admin.sql to use unified tables

Backend Repositories:
- company_repository.go: use companies table with INT id
- user_repository.go: use users/user_roles with INT id conversion

Seeders:
- All seeders now use companies/users/user_roles tables
- Removed all core_* table insertions
- Query companies by slug to get SERIAL id

This eliminates the redundancy between core_* and legacy tables.
This commit is contained in:
Tiago Yamamoto 2025-12-24 11:06:31 -03:00
parent af2719f2e6
commit 7d99e77468
11 changed files with 269 additions and 210 deletions

View file

@ -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

View file

@ -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
}

View file

@ -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;

View file

@ -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)';

View file

@ -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

View file

@ -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;

View file

@ -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'];

View file

@ -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)'];

View file

@ -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) {

View file

@ -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,