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:
parent
af2719f2e6
commit
7d99e77468
11 changed files with 269 additions and 210 deletions
|
|
@ -4,9 +4,10 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
|
||||||
"github.com/rede5/gohorsejobs/backend/internal/core/domain/entity"
|
"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) {
|
func (r *CompanyRepository) Save(ctx context.Context, company *entity.Company) (*entity.Company, error) {
|
||||||
if company.ID == "" {
|
// companies table uses SERIAL id, we let DB generate it
|
||||||
company.ID = uuid.New().String()
|
|
||||||
}
|
|
||||||
|
|
||||||
query := `
|
query := `
|
||||||
INSERT INTO core_companies (id, name, document, contact, status, created_at, updated_at)
|
INSERT INTO companies (name, slug, type, document, email, description, verified, active, created_at, updated_at)
|
||||||
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
|
||||||
RETURNING id
|
RETURNING id
|
||||||
`
|
`
|
||||||
|
|
||||||
|
slug := company.Name // TODO: slugify function
|
||||||
|
var id int
|
||||||
err := r.db.QueryRowContext(ctx, query,
|
err := r.db.QueryRowContext(ctx, query,
|
||||||
company.ID,
|
|
||||||
company.Name,
|
company.Name,
|
||||||
|
slug,
|
||||||
|
"company",
|
||||||
company.Document,
|
company.Document,
|
||||||
company.Contact,
|
company.Contact, // mapped to email
|
||||||
company.Status,
|
"{}", // description as JSON
|
||||||
|
true, // verified
|
||||||
|
company.Status == "ACTIVE",
|
||||||
company.CreatedAt,
|
company.CreatedAt,
|
||||||
company.UpdatedAt,
|
company.UpdatedAt,
|
||||||
).Scan(&company.ID)
|
).Scan(&id)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
company.ID = strconv.Itoa(id)
|
||||||
return company, nil
|
return company, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *CompanyRepository) FindByID(ctx context.Context, id string) (*entity.Company, error) {
|
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`
|
query := `SELECT id, name, document, email,
|
||||||
row := r.db.QueryRowContext(ctx, query, id)
|
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{}
|
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 {
|
if err == sql.ErrNoRows {
|
||||||
return nil, errors.New("company not found")
|
return nil, errors.New("company not found")
|
||||||
}
|
}
|
||||||
|
c.ID = strconv.Itoa(dbID)
|
||||||
return c, err
|
return c, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *CompanyRepository) Update(ctx context.Context, company *entity.Company) (*entity.Company, error) {
|
func (r *CompanyRepository) Update(ctx context.Context, company *entity.Company) (*entity.Company, error) {
|
||||||
company.UpdatedAt = time.Now()
|
company.UpdatedAt = time.Now()
|
||||||
|
|
||||||
|
numID, err := strconv.Atoi(company.ID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("invalid company id: %s", company.ID)
|
||||||
|
}
|
||||||
|
|
||||||
query := `
|
query := `
|
||||||
UPDATE core_companies
|
UPDATE companies
|
||||||
SET name=$1, document=$2, contact=$3, status=$4, updated_at=$5
|
SET name=$1, document=$2, email=$3, active=$4, updated_at=$5
|
||||||
WHERE id=$6
|
WHERE id=$6
|
||||||
`
|
`
|
||||||
_, err := r.db.ExecContext(ctx, query,
|
_, err = r.db.ExecContext(ctx, query,
|
||||||
company.Name,
|
company.Name,
|
||||||
company.Document,
|
company.Document,
|
||||||
company.Contact,
|
company.Contact,
|
||||||
company.Status,
|
company.Status == "ACTIVE",
|
||||||
company.UpdatedAt,
|
company.UpdatedAt,
|
||||||
company.ID,
|
numID,
|
||||||
)
|
)
|
||||||
return company, err
|
return company, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *CompanyRepository) Delete(ctx context.Context, id string) error {
|
func (r *CompanyRepository) Delete(ctx context.Context, id string) error {
|
||||||
query := `DELETE FROM core_companies WHERE id = $1`
|
numID, err := strconv.Atoi(id)
|
||||||
_, err := r.db.ExecContext(ctx, query, 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
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *CompanyRepository) FindAll(ctx context.Context) ([]*entity.Company, error) {
|
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)
|
rows, err := r.db.QueryContext(ctx, query)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
@ -92,9 +120,11 @@ func (r *CompanyRepository) FindAll(ctx context.Context) ([]*entity.Company, err
|
||||||
var companies []*entity.Company
|
var companies []*entity.Company
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
c := &entity.Company{}
|
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
|
return nil, err
|
||||||
}
|
}
|
||||||
|
c.ID = strconv.Itoa(dbID)
|
||||||
companies = append(companies, c)
|
companies = append(companies, c)
|
||||||
}
|
}
|
||||||
return companies, nil
|
return companies, nil
|
||||||
|
|
|
||||||
|
|
@ -3,10 +3,10 @@ package postgres
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"encoding/json"
|
"fmt"
|
||||||
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
|
||||||
"github.com/rede5/gohorsejobs/backend/internal/core/domain/entity"
|
"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) {
|
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)
|
tx, err := r.db.BeginTx(ctx, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
defer tx.Rollback()
|
defer tx.Rollback()
|
||||||
|
|
||||||
// Serialize metadata
|
// Convert tenant_id from string to int
|
||||||
metadata, err := json.Marshal(user.Metadata)
|
var tenantID *int
|
||||||
if err != nil {
|
if user.TenantID != "" {
|
||||||
return nil, err
|
tid, err := strconv.Atoi(user.TenantID)
|
||||||
|
if err == nil {
|
||||||
|
tenantID = &tid
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 1. Insert User
|
// 1. Insert User - users table has SERIAL id
|
||||||
query := `
|
query := `
|
||||||
INSERT INTO core_users (id, tenant_id, name, email, password_hash, status, metadata, created_at, updated_at)
|
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)
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
|
||||||
|
RETURNING id
|
||||||
`
|
`
|
||||||
_, err = tx.ExecContext(ctx, query,
|
|
||||||
user.ID,
|
var id int
|
||||||
user.TenantID,
|
// 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.Name,
|
||||||
user.Email,
|
user.Email,
|
||||||
user.PasswordHash,
|
user.Name,
|
||||||
|
tenantID,
|
||||||
user.Status,
|
user.Status,
|
||||||
metadata,
|
|
||||||
user.CreatedAt,
|
user.CreatedAt,
|
||||||
user.UpdatedAt,
|
user.UpdatedAt,
|
||||||
)
|
).Scan(&id)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
user.ID = strconv.Itoa(id)
|
||||||
|
|
||||||
// 2. Insert Roles
|
// 2. Insert Roles into user_roles table
|
||||||
if len(user.Roles) > 0 {
|
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 {
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
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) {
|
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)
|
row := r.db.QueryRowContext(ctx, query, email)
|
||||||
|
|
||||||
u := &entity.User{}
|
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 != nil {
|
||||||
if err == sql.ErrNoRows {
|
if err == sql.ErrNoRows {
|
||||||
return nil, nil // Return nil if not found
|
return nil, nil // Return nil if not found
|
||||||
}
|
}
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
u.ID = strconv.Itoa(dbID)
|
||||||
u.Roles, _ = r.getRoles(ctx, u.ID)
|
u.Roles, _ = r.getRoles(ctx, dbID)
|
||||||
return u, nil
|
return u, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *UserRepository) FindByID(ctx context.Context, id string) (*entity.User, error) {
|
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`
|
numID, err := strconv.Atoi(id)
|
||||||
row := r.db.QueryRowContext(ctx, query, 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{}
|
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 != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
u.ID = strconv.Itoa(dbID)
|
||||||
u.Roles, _ = r.getRoles(ctx, u.ID)
|
u.Roles, _ = r.getRoles(ctx, dbID)
|
||||||
return u, nil
|
return u, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *UserRepository) FindAllByTenant(ctx context.Context, tenantID string, limit, offset int) ([]*entity.User, int, error) {
|
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
|
var total int
|
||||||
countQuery := `SELECT COUNT(*) FROM core_users WHERE tenant_id = $1`
|
countQuery := `SELECT COUNT(*) FROM users WHERE tenant_id = $1`
|
||||||
if err := r.db.QueryRowContext(ctx, countQuery, tenantID).Scan(&total); err != nil {
|
if err := r.db.QueryRowContext(ctx, countQuery, numTenantID).Scan(&total); err != nil {
|
||||||
return nil, 0, err
|
return nil, 0, err
|
||||||
}
|
}
|
||||||
|
|
||||||
query := `SELECT id, tenant_id, name, email, password_hash, status, created_at, updated_at
|
query := `SELECT id, COALESCE(tenant_id::text, ''), COALESCE(name, full_name, ''),
|
||||||
FROM core_users
|
COALESCE(email, identifier), password_hash, COALESCE(status, 'active'), created_at, updated_at
|
||||||
|
FROM users
|
||||||
WHERE tenant_id = $1
|
WHERE tenant_id = $1
|
||||||
ORDER BY created_at DESC
|
ORDER BY created_at DESC
|
||||||
LIMIT $2 OFFSET $3`
|
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 {
|
if err != nil {
|
||||||
return nil, 0, err
|
return nil, 0, err
|
||||||
}
|
}
|
||||||
|
|
@ -125,31 +153,40 @@ func (r *UserRepository) FindAllByTenant(ctx context.Context, tenantID string, l
|
||||||
var users []*entity.User
|
var users []*entity.User
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
u := &entity.User{}
|
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
|
return nil, 0, err
|
||||||
}
|
}
|
||||||
// Populate roles N+1? Ideally join, but for now simple
|
u.ID = strconv.Itoa(dbID)
|
||||||
u.Roles, _ = r.getRoles(ctx, u.ID)
|
u.Roles, _ = r.getRoles(ctx, dbID)
|
||||||
users = append(users, u)
|
users = append(users, u)
|
||||||
}
|
}
|
||||||
return users, total, nil
|
return users, total, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *UserRepository) Update(ctx context.Context, user *entity.User) (*entity.User, error) {
|
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()
|
user.UpdatedAt = time.Now()
|
||||||
query := `UPDATE core_users SET name=$1, email=$2, status=$3, updated_at=$4 WHERE id=$5`
|
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, user.ID)
|
_, err = r.db.ExecContext(ctx, query, user.Name, user.Email, user.Status, user.UpdatedAt, numID)
|
||||||
return user, err
|
return user, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *UserRepository) Delete(ctx context.Context, id string) error {
|
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
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *UserRepository) getRoles(ctx context.Context, userID string) ([]entity.Role, error) {
|
func (r *UserRepository) getRoles(ctx context.Context, userID int) ([]entity.Role, error) {
|
||||||
rows, err := r.db.QueryContext(ctx, `SELECT role FROM core_user_roles WHERE user_id = $1`, userID)
|
rows, err := r.db.QueryContext(ctx, `SELECT role FROM user_roles WHERE user_id = $1`, userID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,38 +1,39 @@
|
||||||
-- Migration: Create Super Admin and System Tenant
|
-- Migration: Create Super Admin and System Company
|
||||||
-- Description: Inserts the default System Tenant and Super Admin user.
|
-- Description: Inserts the default System Company and Super Admin user.
|
||||||
-- Use fixed UUIDs for reproducibility in dev environments.
|
-- Uses unified tables (companies, users, user_roles)
|
||||||
|
|
||||||
-- 1. Insert System Tenant
|
-- 1. Insert System Company (for SuperAdmin context)
|
||||||
INSERT INTO core_companies (id, name, document, contact, status, created_at, updated_at)
|
INSERT INTO companies (name, slug, type, document, email, description, verified, active)
|
||||||
VALUES (
|
VALUES (
|
||||||
'00000000-0000-0000-0000-000000000001', -- Fixed System Tenant ID
|
'GoHorse System',
|
||||||
'System Tenant (SuperAdmin Context)',
|
'gohorse-system',
|
||||||
'SYSTEM',
|
'system',
|
||||||
'admin@system.local',
|
'00.000.000/0001-91',
|
||||||
'ACTIVE',
|
'admin@gohorsejobs.com',
|
||||||
CURRENT_TIMESTAMP,
|
'{"tagline": "System Administration Tenant"}',
|
||||||
CURRENT_TIMESTAMP
|
true,
|
||||||
) ON CONFLICT (id) DO NOTHING;
|
true
|
||||||
|
) ON CONFLICT (slug) DO NOTHING;
|
||||||
|
|
||||||
-- 2. Insert Super Admin User
|
-- 2. Insert Super Admin User
|
||||||
-- WARNING: This hash is generated WITHOUT PASSWORD_PEPPER.
|
-- WARNING: This hash is generated WITHOUT PASSWORD_PEPPER.
|
||||||
-- For development only. Use seeder-api for proper user creation.
|
-- For development only. Use seeder-api for proper user creation.
|
||||||
-- Password: "password123" (BCrypt hash without pepper)
|
-- Password: "Admin@2025!" (should be created by seeder with proper pepper)
|
||||||
INSERT INTO core_users (id, tenant_id, name, email, password_hash, status, created_at, updated_at)
|
INSERT INTO users (identifier, password_hash, role, full_name, email, name, status, tenant_id)
|
||||||
VALUES (
|
SELECT
|
||||||
'00000000-0000-0000-0000-000000000002', -- Fixed SuperAdmin User ID
|
'superadmin',
|
||||||
'00000000-0000-0000-0000-000000000001', -- Link to System Tenant
|
'$2a$10$UWrE9xN39lVagJHlXZsxwOVI3NRSEd1VJ6UzMblW6LOxNmsOZtj9K', -- placeholder, seeder will update
|
||||||
|
'superadmin',
|
||||||
'Super Administrator',
|
'Super Administrator',
|
||||||
'admin@todai.jobs',
|
'admin@gohorsejobs.com',
|
||||||
'$2a$10$UWrE9xN39lVagJHlXZsxwOVI3NRSEd1VJ6UzMblW6LOxNmsOZtj9K', -- "password123"
|
'Super Administrator',
|
||||||
'ACTIVE',
|
'active',
|
||||||
CURRENT_TIMESTAMP,
|
c.id
|
||||||
CURRENT_TIMESTAMP
|
FROM companies c WHERE c.slug = 'gohorse-system'
|
||||||
) ON CONFLICT (id) DO NOTHING;
|
ON CONFLICT (identifier) DO NOTHING;
|
||||||
|
|
||||||
-- 3. Assign Super Admin Role
|
-- 3. Assign Super Admin Role
|
||||||
INSERT INTO core_user_roles (user_id, role)
|
INSERT INTO user_roles (user_id, role)
|
||||||
VALUES (
|
SELECT u.id, 'SUPER_ADMIN'
|
||||||
'00000000-0000-0000-0000-000000000002',
|
FROM users u WHERE u.identifier = 'superadmin'
|
||||||
'SUPER_ADMIN'
|
ON CONFLICT (user_id, role) DO NOTHING;
|
||||||
) ON CONFLICT (user_id, role) DO NOTHING;
|
|
||||||
|
|
|
||||||
33
backend/migrations/020_unify_schema.sql
Normal file
33
backend/migrations/020_unify_schema.sql
Normal 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)';
|
||||||
|
|
@ -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'
|
'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');
|
console.log(' ✓ ACME Corporation criada');
|
||||||
|
|
||||||
// 2. Get ACME company ID (use coreId for FK)
|
// 2. Get ACME company ID from companies table
|
||||||
const acmeCompanyId = acmeCoreId;
|
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)
|
// 3. Get seed user from users table (for FK)
|
||||||
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;
|
const seedUserId = seedUserRes.rows[0]?.id;
|
||||||
|
|
||||||
// 4. Seed 69 jobs
|
// 4. Seed 69 jobs
|
||||||
|
|
|
||||||
|
|
@ -87,24 +87,16 @@ export async function seedCompanies() {
|
||||||
true,
|
true,
|
||||||
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(`
|
await pool.query(`
|
||||||
INSERT INTO core_companies (id, name, document, status)
|
INSERT INTO companies (name, slug, type, document, email, description, verified, active)
|
||||||
VALUES ('00000000-0000-0000-0000-000000000000', 'GoHorse System', '00.000.000/0001-91', 'ACTIVE')
|
VALUES ('GoHorse System', 'gohorse-system', 'system', '00.000.000/0001-91', 'admin@gohorsejobs.com', '{"tagline": "System Administration"}', true, true)
|
||||||
ON CONFLICT (id) DO NOTHING
|
ON CONFLICT (slug) DO NOTHING
|
||||||
`);
|
`);
|
||||||
|
|
||||||
console.log(` ✓ ${companyData.length} companies seeded (Legacy & Core)`);
|
console.log(` ✓ ${companyData.length + 1} companies seeded`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(' ❌ Error seeding companies:', error.message);
|
console.error(' ❌ Error seeding companies:', error.message);
|
||||||
throw error;
|
throw error;
|
||||||
|
|
|
||||||
|
|
@ -263,7 +263,7 @@ async function createCompanyAndJobs(companyData, jobs) {
|
||||||
const regionsRes = await pool.query('SELECT id FROM regions LIMIT 1');
|
const regionsRes = await pool.query('SELECT id FROM regions LIMIT 1');
|
||||||
const defaultRegionId = regionsRes.rows[0]?.id || null;
|
const defaultRegionId = regionsRes.rows[0]?.id || null;
|
||||||
|
|
||||||
// Create Company
|
// Create Company (companies uses SERIAL id)
|
||||||
await pool.query(`
|
await pool.query(`
|
||||||
INSERT INTO companies (name, slug, type, document, address, region_id, phone, email, website, description, verified, active)
|
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)
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)
|
||||||
|
|
@ -283,17 +283,11 @@ async function createCompanyAndJobs(companyData, jobs) {
|
||||||
true
|
true
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Core Company
|
// Get company ID from companies table (SERIAL id)
|
||||||
await pool.query(`
|
const companyRes = await pool.query("SELECT id FROM companies WHERE slug = $1", [companyData.slug]);
|
||||||
INSERT INTO core_companies (id, name, document, status)
|
const companyId = companyRes.rows[0]?.id;
|
||||||
VALUES ($1, $2, $3, 'ACTIVE')
|
|
||||||
ON CONFLICT (id) DO NOTHING
|
|
||||||
`, [companyData.coreId, companyData.name, companyData.cnpj]);
|
|
||||||
|
|
||||||
// Get company ID (use coreId for FK reference)
|
const seedUserRes = await pool.query("SELECT id FROM users LIMIT 1");
|
||||||
const companyId = companyData.coreId;
|
|
||||||
|
|
||||||
const seedUserRes = await pool.query("SELECT id FROM core_users LIMIT 1");
|
|
||||||
const seedUserId = seedUserRes.rows[0]?.id || 1;
|
const seedUserId = seedUserRes.rows[0]?.id || 1;
|
||||||
|
|
||||||
const workModes = ['onsite', 'hybrid', 'remote'];
|
const workModes = ['onsite', 'hybrid', 'remote'];
|
||||||
|
|
|
||||||
|
|
@ -177,18 +177,12 @@ export async function seedStarkIndustries() {
|
||||||
true
|
true
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Core Company
|
// Get company ID (SERIAL) from companies table
|
||||||
await pool.query(`
|
const companyRes = await pool.query("SELECT id FROM companies WHERE slug = 'Stark Industries'");
|
||||||
INSERT INTO core_companies (id, name, document, status)
|
const companyId = companyRes.rows[0]?.id;
|
||||||
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 seed user
|
// 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;
|
const seedUserId = seedUserRes.rows[0]?.id || 1;
|
||||||
|
|
||||||
// Create jobs
|
// Create jobs
|
||||||
|
|
@ -265,7 +259,7 @@ export async function seedLosPollosHermanos() {
|
||||||
|
|
||||||
// Core Company
|
// Core Company
|
||||||
await pool.query(`
|
await pool.query(`
|
||||||
INSERT INTO core_companies (id, name, document, status)
|
INSERT INTO companies (id, name, document, status)
|
||||||
VALUES ($1, $2, $3, 'ACTIVE')
|
VALUES ($1, $2, $3, 'ACTIVE')
|
||||||
ON CONFLICT (id) DO NOTHING
|
ON CONFLICT (id) DO NOTHING
|
||||||
`, ['66666666-6666-6666-6666-666666666666', 'Los Pollos Hermanos', '66.666.666/0001-66']);
|
`, ['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)
|
// Get company ID (use coreId directly)
|
||||||
const companyId = '66666666-6666-6666-6666-666666666666';
|
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 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)'];
|
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
|
// Core Company
|
||||||
await pool.query(`
|
await pool.query(`
|
||||||
INSERT INTO core_companies (id, name, document, status)
|
INSERT INTO companies (id, name, document, status)
|
||||||
VALUES ($1, $2, $3, 'ACTIVE')
|
VALUES ($1, $2, $3, 'ACTIVE')
|
||||||
ON CONFLICT (id) DO NOTHING
|
ON CONFLICT (id) DO NOTHING
|
||||||
`, ['88888888-8888-8888-8888-888888888888', 'Springfield Nuclear Power Plant', '88.888.888/0001-88']);
|
`, ['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)
|
// Get company ID (use coreId directly)
|
||||||
const companyId = '88888888-8888-8888-8888-888888888888';
|
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 seedUserId = seedUserRes.rows[0]?.id || 1;
|
||||||
|
|
||||||
const benefits = ['Tickets de Cafeteria', 'Dosímetro Pessoal', 'Desconto na Taverna do Moe', 'Estacionamento (longe do reator)'];
|
const benefits = ['Tickets de Cafeteria', 'Dosímetro Pessoal', 'Desconto na Taverna do Moe', 'Estacionamento (longe do reator)'];
|
||||||
|
|
|
||||||
|
|
@ -78,8 +78,8 @@ export async function seedNotifications() {
|
||||||
console.log('🔔 Seeding notifications...');
|
console.log('🔔 Seeding notifications...');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Get users from core_users
|
// Get users from users
|
||||||
const usersRes = await pool.query('SELECT id FROM core_users LIMIT 5');
|
const usersRes = await pool.query('SELECT id FROM users LIMIT 5');
|
||||||
const users = usersRes.rows;
|
const users = usersRes.rows;
|
||||||
|
|
||||||
if (users.length === 0) {
|
if (users.length === 0) {
|
||||||
|
|
|
||||||
|
|
@ -1,110 +1,105 @@
|
||||||
|
|
||||||
import bcrypt from 'bcrypt';
|
import bcrypt from 'bcrypt';
|
||||||
import crypto from 'crypto';
|
|
||||||
import { pool } from '../db.js';
|
import { pool } from '../db.js';
|
||||||
|
|
||||||
// Get pepper from environment - MUST match backend PASSWORD_PEPPER
|
// Get pepper from environment - MUST match backend PASSWORD_PEPPER
|
||||||
const PASSWORD_PEPPER = process.env.PASSWORD_PEPPER || '';
|
const PASSWORD_PEPPER = process.env.PASSWORD_PEPPER || '';
|
||||||
|
|
||||||
export async function seedUsers() {
|
export async function seedUsers() {
|
||||||
console.log('👤 Seeding users (Core Architecture)...');
|
console.log('👤 Seeding users (Unified Architecture)...');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Fetch core companies to map users
|
// Fetch companies to map users (now using companies table, not core_companies)
|
||||||
const companiesResult = await pool.query('SELECT id, name FROM core_companies');
|
const companiesResult = await pool.query('SELECT id, name, slug FROM companies');
|
||||||
const companyMap = {}; // name -> id
|
const companyMap = {}; // name -> id
|
||||||
companiesResult.rows.forEach(c => companyMap[c.name] = c.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!)
|
// 1. Create SuperAdmin (Requested: superadmin / Admin@2025!)
|
||||||
const superAdminPassword = await bcrypt.hash('Admin@2025!' + PASSWORD_PEPPER, 10);
|
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(`
|
const result = await pool.query(`
|
||||||
INSERT INTO core_users (id, tenant_id, name, email, password_hash, status)
|
INSERT INTO users (identifier, password_hash, role, full_name, email, name, tenant_id, status)
|
||||||
VALUES ($1, $2, $3, $4, $5, 'ACTIVE')
|
VALUES ($1, $2, 'superadmin', $3, $4, $5, $6, 'active')
|
||||||
ON CONFLICT (tenant_id, email) DO UPDATE SET password_hash = EXCLUDED.password_hash
|
ON CONFLICT (identifier) DO UPDATE SET password_hash = EXCLUDED.password_hash
|
||||||
RETURNING id
|
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(`
|
await pool.query(`
|
||||||
INSERT INTO core_user_roles (user_id, role)
|
INSERT INTO user_roles (user_id, role)
|
||||||
VALUES ($1, 'superadmin')
|
VALUES ($1, 'superadmin')
|
||||||
ON CONFLICT (user_id, role) DO NOTHING
|
ON CONFLICT (user_id, role) DO NOTHING
|
||||||
`, [actualSuperAdminId]);
|
`, [superAdminId]);
|
||||||
|
|
||||||
console.log(' ✓ SuperAdmin created (superadmin)');
|
console.log(' ✓ SuperAdmin created (superadmin)');
|
||||||
|
|
||||||
// 2. Create Company Admins
|
// 2. Create Company Admins
|
||||||
const companyAdmins = [
|
const companyAdmins = [
|
||||||
// Requested: takeshi_yamamoto / Takeshi@2025 (Company Admin)
|
{ identifier: 'takeshi_yamamoto', fullName: 'Takeshi Yamamoto', company: 'TechCorp', email: 'takeshi@techcorp.com', pass: 'Takeshi@2025', roles: ['admin', 'company'] },
|
||||||
{ identifier: 'takeshi_yamamoto', fullName: 'Takeshi Yamamoto', company: 'TechCorp', email: 'takeshi_yamamoto', pass: 'Takeshi@2025', roles: ['admin', 'company'] },
|
|
||||||
{ identifier: 'kenji', fullName: 'Kenji Tanaka', company: 'AppMakers', email: 'kenji@appmakers.mobile', 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@designhub.com', pass: 'User@2025', roles: ['recruiter', 'company'] }
|
||||||
{ identifier: 'maria_santos', fullName: 'Maria Santos', company: 'DesignHub', email: 'maria_santos', pass: 'User@2025', roles: ['recruiter', 'company'] }
|
|
||||||
];
|
];
|
||||||
|
|
||||||
for (const admin of companyAdmins) {
|
for (const admin of companyAdmins) {
|
||||||
const hash = await bcrypt.hash(admin.pass + PASSWORD_PEPPER, 10);
|
const hash = await bcrypt.hash(admin.pass + PASSWORD_PEPPER, 10);
|
||||||
const userId = crypto.randomUUID();
|
|
||||||
const tenantId = companyMap[admin.company];
|
const tenantId = companyMap[admin.company];
|
||||||
|
|
||||||
if (!tenantId) {
|
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;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await pool.query(`
|
const result = await pool.query(`
|
||||||
INSERT INTO core_users (id, tenant_id, name, email, password_hash, status)
|
INSERT INTO users (identifier, password_hash, role, full_name, email, name, tenant_id, status)
|
||||||
VALUES ($1, $2, $3, $4, $5, 'ACTIVE')
|
VALUES ($1, $2, $3, $4, $5, $6, $7, 'active')
|
||||||
ON CONFLICT (tenant_id, email) DO UPDATE SET password_hash = EXCLUDED.password_hash
|
ON CONFLICT (identifier) DO UPDATE SET password_hash = EXCLUDED.password_hash
|
||||||
RETURNING id
|
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) {
|
for (const role of admin.roles) {
|
||||||
await pool.query(`
|
await pool.query(`
|
||||||
INSERT INTO core_user_roles (user_id, role)
|
INSERT INTO user_roles (user_id, role)
|
||||||
VALUES ($1, $2)
|
VALUES ($1, $2)
|
||||||
ON CONFLICT (user_id, role) DO NOTHING
|
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)
|
// 3. Create Candidates (Job Seekers)
|
||||||
// Requested: paulo_santos / User@2025
|
|
||||||
const candidates = [
|
const candidates = [
|
||||||
{ fullName: 'Paulo Santos', email: 'paulo_santos', pass: 'User@2025' },
|
{ identifier: 'paulo_santos', fullName: 'Paulo Santos', email: 'paulo@email.com', pass: 'User@2025' },
|
||||||
{ fullName: 'Maria Reyes', email: 'maria@email.com', pass: 'User@2025' }
|
{ identifier: 'maria_email', fullName: 'Maria Reyes', email: 'maria@email.com', pass: 'User@2025' }
|
||||||
];
|
];
|
||||||
|
|
||||||
for (const cand of candidates) {
|
for (const cand of candidates) {
|
||||||
const hash = await bcrypt.hash(cand.pass + PASSWORD_PEPPER, 10);
|
const hash = await bcrypt.hash(cand.pass + PASSWORD_PEPPER, 10);
|
||||||
const userId = crypto.randomUUID();
|
|
||||||
|
|
||||||
const result = await pool.query(`
|
const result = await pool.query(`
|
||||||
INSERT INTO core_users (id, tenant_id, name, email, password_hash, status)
|
INSERT INTO users (identifier, password_hash, role, full_name, email, name, status)
|
||||||
VALUES ($1, $2, $3, $4, $5, 'ACTIVE')
|
VALUES ($1, $2, 'jobSeeker', $3, $4, $5, 'active')
|
||||||
ON CONFLICT (tenant_id, email) DO UPDATE SET password_hash = EXCLUDED.password_hash
|
ON CONFLICT (identifier) DO UPDATE SET password_hash = EXCLUDED.password_hash
|
||||||
RETURNING id
|
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(`
|
await pool.query(`
|
||||||
INSERT INTO core_user_roles (user_id, role)
|
INSERT INTO user_roles (user_id, role)
|
||||||
VALUES ($1, 'candidate')
|
VALUES ($1, 'candidate')
|
||||||
ON CONFLICT (user_id, role) DO NOTHING
|
ON CONFLICT (user_id, role) DO NOTHING
|
||||||
`, [actualUserId]);
|
`, [userId]);
|
||||||
console.log(` ✓ Candidate created: ${cand.email}`);
|
|
||||||
|
console.log(` ✓ Candidate created: ${cand.identifier}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('👤 Seeding legacy candidates...');
|
console.log('👤 Seeding legacy candidates...');
|
||||||
|
|
@ -120,7 +115,7 @@ export async function seedUsers() {
|
||||||
title: 'Full Stack Developer',
|
title: 'Full Stack Developer',
|
||||||
experience: '5 years of experience',
|
experience: '5 years of experience',
|
||||||
skills: ['React', 'Node.js', 'TypeScript', 'AWS', 'Docker'],
|
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.'
|
objective: 'Grow as a full stack developer building scalable products.'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -133,7 +128,7 @@ export async function seedUsers() {
|
||||||
title: 'UX/UI Designer',
|
title: 'UX/UI Designer',
|
||||||
experience: '3 years of experience',
|
experience: '3 years of experience',
|
||||||
skills: ['Figma', 'Adobe XD', 'UI Design', 'Prototyping', 'Design Systems'],
|
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.'
|
objective: 'Design intuitive experiences for web and mobile products.'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -146,7 +141,7 @@ export async function seedUsers() {
|
||||||
title: 'Data Engineer',
|
title: 'Data Engineer',
|
||||||
experience: '7 years of experience',
|
experience: '7 years of experience',
|
||||||
skills: ['Python', 'SQL', 'Spark', 'Machine Learning', 'Data Visualization'],
|
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.'
|
objective: 'Build robust data pipelines and analytics products.'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -159,7 +154,7 @@ export async function seedUsers() {
|
||||||
title: 'Product Manager',
|
title: 'Product Manager',
|
||||||
experience: '6 years of experience',
|
experience: '6 years of experience',
|
||||||
skills: ['Product Management', 'Agile', 'Scrum', 'Data Analysis', 'User Research'],
|
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.'
|
objective: 'Lead cross-functional teams to deliver customer-centric products.'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -172,7 +167,7 @@ export async function seedUsers() {
|
||||||
title: 'DevOps Engineer',
|
title: 'DevOps Engineer',
|
||||||
experience: '4 years of experience',
|
experience: '4 years of experience',
|
||||||
skills: ['Docker', 'Kubernetes', 'AWS', 'Terraform', 'CI/CD'],
|
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.'
|
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);
|
const hash = await bcrypt.hash('User@2025' + PASSWORD_PEPPER, 10);
|
||||||
await pool.query(`
|
await pool.query(`
|
||||||
INSERT INTO users (
|
INSERT INTO users (
|
||||||
identifier,
|
identifier, password_hash, role, full_name, email, name, phone,
|
||||||
password_hash,
|
city, state, title, experience, skills, objective, bio, status
|
||||||
role,
|
) VALUES ($1, $2, 'jobSeeker', $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, 'active')
|
||||||
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)
|
|
||||||
ON CONFLICT (identifier) DO UPDATE SET
|
ON CONFLICT (identifier) DO UPDATE SET
|
||||||
full_name = EXCLUDED.full_name,
|
full_name = EXCLUDED.full_name,
|
||||||
email = EXCLUDED.email,
|
email = EXCLUDED.email,
|
||||||
|
|
@ -211,6 +195,7 @@ export async function seedUsers() {
|
||||||
hash,
|
hash,
|
||||||
cand.fullName,
|
cand.fullName,
|
||||||
cand.email,
|
cand.email,
|
||||||
|
cand.fullName,
|
||||||
cand.phone,
|
cand.phone,
|
||||||
cand.city,
|
cand.city,
|
||||||
cand.state,
|
cand.state,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue