refactor(roles): rename companyAdmin->admin and jobSeeker->candidate

This commit is contained in:
Tiago Yamamoto 2025-12-24 13:30:50 -03:00
parent 1b4f1d1555
commit c1078563df
23 changed files with 115 additions and 80 deletions

View file

@ -75,10 +75,10 @@ O endpoint `/jobs` suporta filtros avançados via query params:
| Método | Endpoint | Roles | Descrição | | Método | Endpoint | Roles | Descrição |
|--------|----------|-------|-----------| |--------|----------|-------|-----------|
| `GET` | `/api/v1/users` | `superadmin`, `companyAdmin` | Listar usuários | | `GET` | `/api/v1/users` | `superadmin`, `admin` | Listar usuários |
| `POST` | `/api/v1/users` | `superadmin`, `companyAdmin` | Criar usuário | | `POST` | `/api/v1/users` | `superadmin`, `admin` | Criar usuário |
| `DELETE` | `/api/v1/users/{id}` | `superadmin` | Deletar usuário | | `DELETE` | `/api/v1/users/{id}` | `superadmin` | Deletar usuário |
| `POST` | `/jobs` | `companyAdmin`, `recruiter` | Criar vaga | | `POST` | `/jobs` | `admin`, `recruiter` | Criar vaga |
--- ---

View file

@ -2,6 +2,20 @@ package entity
import "time" import "time"
// Role type alias
type RoleString string
const (
// RoleSuperAdmin is the platform administrator
RoleSuperAdmin = "superadmin"
// RoleAdmin is the company administrator (formerly admin)
RoleAdmin = "admin"
// RoleRecruiter is a recruiter within a company
RoleRecruiter = "recruiter"
// RoleCandidate is a job seeker (formerly candidate)
RoleCandidate = "candidate"
)
// User represents a user within a specific Tenant (Company). // User represents a user within a specific Tenant (Company).
type User struct { type User struct {
ID string `json:"id"` ID string `json:"id"`

View file

@ -31,7 +31,7 @@ type UserInfo struct {
type CompanyInfo struct { type CompanyInfo struct {
ID int `json:"id"` ID int `json:"id"`
Name string `json:"name"` Name string `json:"name"`
Role string `json:"role"` // Role in this company (companyAdmin or recruiter) Role string `json:"role"` // Role in this company (admin or recruiter)
} }
// RegisterRequest represents user registration // RegisterRequest represents user registration
@ -44,7 +44,7 @@ type RegisterRequest struct {
LineID *string `json:"lineId,omitempty"` LineID *string `json:"lineId,omitempty"`
Instagram *string `json:"instagram,omitempty"` Instagram *string `json:"instagram,omitempty"`
Language string `json:"language" validate:"required,oneof=pt en es ja"` Language string `json:"language" validate:"required,oneof=pt en es ja"`
Role string `json:"role" validate:"required,oneof=jobSeeker recruiter companyAdmin"` Role string `json:"role" validate:"required,oneof=candidate recruiter admin"`
} }
// User represents a generic user profile // User represents a generic user profile

View file

@ -99,7 +99,7 @@ type UpdateCompanyRequest struct {
type AssignUserToCompanyRequest struct { type AssignUserToCompanyRequest struct {
UserID string `json:"userId" validate:"required"` UserID string `json:"userId" validate:"required"`
CompanyID string `json:"companyId" validate:"required"` CompanyID string `json:"companyId" validate:"required"`
Role string `json:"role" validate:"required,oneof=companyAdmin recruiter"` Role string `json:"role" validate:"required,oneof=admin recruiter"`
Permissions map[string]interface{} `json:"permissions,omitempty"` Permissions map[string]interface{} `json:"permissions,omitempty"`
} }

View file

@ -37,8 +37,8 @@ func (r *UserRepository) Save(ctx context.Context, user *entity.User) (*entity.U
` `
var id string var id string
// Map the first role to the role column, default to 'jobSeeker' // Map the first role to the role column, default to 'candidate'
role := "jobSeeker" role := "candidate"
if len(user.Roles) > 0 { if len(user.Roles) > 0 {
role = user.Roles[0].Name role = user.Roles[0].Name
} }

View file

@ -20,7 +20,7 @@ mux.Handle("/admin", AuthMiddleware(RequireRole("superadmin")(handler)))
**Claims extraídas:** **Claims extraídas:**
- `UserID` - ID do usuário - `UserID` - ID do usuário
- `Role` - Papel (superadmin, companyAdmin, recruiter, jobSeeker) - `Role` - Papel (superadmin, admin, recruiter, candidate)
- `CompanyID` - ID da empresa (se aplicável) - `CompanyID` - ID da empresa (se aplicável)
--- ---

View file

@ -7,7 +7,7 @@ type User struct {
ID string `json:"id" db:"id"` ID string `json:"id" db:"id"`
Identifier string `json:"identifier" db:"identifier"` Identifier string `json:"identifier" db:"identifier"`
PasswordHash string `json:"-" db:"password_hash"` // Never expose password hash in JSON PasswordHash string `json:"-" db:"password_hash"` // Never expose password hash in JSON
Role string `json:"role" db:"role"` // superadmin, companyAdmin, recruiter, jobSeeker Role string `json:"role" db:"role"` // superadmin, admin, recruiter, candidate
// Personal Info // Personal Info
FullName string `json:"fullName" db:"full_name"` FullName string `json:"fullName" db:"full_name"`

View file

@ -11,7 +11,7 @@ type UserCompany struct {
ID string `json:"id" db:"id"` ID string `json:"id" db:"id"`
UserID string `json:"userId" db:"user_id"` UserID string `json:"userId" db:"user_id"`
CompanyID string `json:"companyId" db:"company_id"` CompanyID string `json:"companyId" db:"company_id"`
Role string `json:"role" db:"role"` // companyAdmin, recruiter Role string `json:"role" db:"role"` // admin, recruiter
Permissions JSONMap `json:"permissions,omitempty" db:"permissions"` Permissions JSONMap `json:"permissions,omitempty" db:"permissions"`
CreatedAt time.Time `json:"createdAt" db:"created_at"` CreatedAt time.Time `json:"createdAt" db:"created_at"`
} }

View file

@ -276,7 +276,7 @@ func (s *AdminService) ListCandidates(ctx context.Context) ([]dto.Candidate, dto
query := ` query := `
SELECT id, full_name, email, phone, city, state, title, experience, bio, skills, avatar_url, created_at SELECT id, full_name, email, phone, city, state, title, experience, bio, skills, avatar_url, created_at
FROM users FROM users
WHERE role = 'jobSeeker' WHERE role = 'candidate'
ORDER BY created_at DESC ORDER BY created_at DESC
` `

View file

@ -5,7 +5,7 @@ CREATE TABLE IF NOT EXISTS users (
id UUID PRIMARY KEY DEFAULT uuid_generate_v7(), id UUID PRIMARY KEY DEFAULT uuid_generate_v7(),
identifier VARCHAR(100) UNIQUE NOT NULL, identifier VARCHAR(100) UNIQUE NOT NULL,
password_hash VARCHAR(255) NOT NULL, password_hash VARCHAR(255) NOT NULL,
role VARCHAR(20) NOT NULL CHECK (role IN ('superadmin', 'companyAdmin', 'recruiter', 'jobSeeker')), role VARCHAR(20) NOT NULL CHECK (role IN ('superadmin', 'admin', 'recruiter', 'candidate')),
-- Personal Info -- Personal Info
full_name VARCHAR(255), full_name VARCHAR(255),
@ -32,4 +32,4 @@ CREATE INDEX idx_users_active ON users(active);
-- Comments for documentation -- Comments for documentation
COMMENT ON TABLE users IS 'Stores all system users across all roles'; COMMENT ON TABLE users IS 'Stores all system users across all roles';
COMMENT ON COLUMN users.identifier IS 'Username for login (NOT email)'; COMMENT ON COLUMN users.identifier IS 'Username for login (NOT email)';
COMMENT ON COLUMN users.role IS 'User role: superadmin, companyAdmin, recruiter, or jobSeeker'; COMMENT ON COLUMN users.role IS 'User role: superadmin, admin, recruiter, or candidate';

View file

@ -5,7 +5,7 @@ CREATE TABLE IF NOT EXISTS user_companies (
id UUID PRIMARY KEY DEFAULT uuid_generate_v7(), id UUID PRIMARY KEY DEFAULT uuid_generate_v7(),
user_id UUID NOT NULL, user_id UUID NOT NULL,
company_id UUID NOT NULL, company_id UUID NOT NULL,
role VARCHAR(20) NOT NULL CHECK (role IN ('companyAdmin', 'recruiter')), role VARCHAR(20) NOT NULL CHECK (role IN ('admin', 'recruiter')),
permissions JSONB, -- Optional granular permissions permissions JSONB, -- Optional granular permissions
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
@ -25,5 +25,5 @@ CREATE INDEX idx_user_companies_role ON user_companies(role);
-- Comments -- Comments
COMMENT ON TABLE user_companies IS 'Multi-tenant pivot: links users to companies with specific roles'; COMMENT ON TABLE user_companies IS 'Multi-tenant pivot: links users to companies with specific roles';
COMMENT ON COLUMN user_companies.role IS 'Role within this company: companyAdmin or recruiter'; COMMENT ON COLUMN user_companies.role IS 'Role within this company: admin or recruiter';
COMMENT ON COLUMN user_companies.permissions IS 'Optional JSON object for granular permissions per company'; COMMENT ON COLUMN user_companies.permissions IS 'Optional JSON object for granular permissions per company';

View file

@ -3,7 +3,7 @@
CREATE TABLE IF NOT EXISTS password_resets ( CREATE TABLE IF NOT EXISTS password_resets (
id SERIAL PRIMARY KEY, id SERIAL PRIMARY KEY,
user_id INT NOT NULL, user_id UUID NOT NULL,
token VARCHAR(255) UNIQUE NOT NULL, token VARCHAR(255) UNIQUE NOT NULL,
expires_at TIMESTAMP NOT NULL, expires_at TIMESTAMP NOT NULL,
used BOOLEAN DEFAULT false, used BOOLEAN DEFAULT false,

View file

@ -1,6 +1,3 @@
DROP TABLE IF EXISTS ticket_messages;
DROP TABLE IF EXISTS tickets;
CREATE TABLE tickets ( CREATE TABLE tickets (
id UUID PRIMARY KEY DEFAULT uuid_generate_v7(), id UUID PRIMARY KEY DEFAULT uuid_generate_v7(),
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,

View file

@ -18,9 +18,9 @@ Complete API reference with routes, permissions, and modules.
| Role | Code | Level | Description | | Role | Code | Level | Description |
|------|------|-------|-------------| |------|------|-------|-------------|
| **SuperAdmin** | `superadmin` | 0 | Platform administrator | | **SuperAdmin** | `superadmin` | 0 | Platform administrator |
| **CompanyAdmin** | `companyAdmin` | 1 | Company administrator | | **Admin** | `admin` | 1 | Company administrator |
| **Recruiter** | `recruiter` | 2 | Job poster | | **Recruiter** | `recruiter` | 2 | Job poster |
| **JobSeeker** | `jobSeeker` | 3 | Candidate | | **Candidate** | `candidate` | 3 | Candidate |
| **Guest** | - | - | No authentication | | **Guest** | - | - | No authentication |
--- ---
@ -124,7 +124,7 @@ POST /api/v1/users
``` ```
| Field | Auth | Roles | Description | | Field | Auth | Roles | Description |
|-------|------|-------|-------------| |-------|------|-------|-------------|
| Protected | ✅ | superadmin, companyAdmin | Create new user | | Protected | ✅ | superadmin, admin | Create new user |
### Update User ### Update User
```http ```http
@ -186,7 +186,7 @@ POST /api/v1/jobs
``` ```
| Field | Auth | Roles | Description | | Field | Auth | Roles | Description |
|-------|------|-------|-------------| |-------|------|-------|-------------|
| Public* | ❌ | companyAdmin, recruiter | Create new job posting | | Public* | ❌ | admin, recruiter | Create new job posting |
### Update Job ### Update Job
```http ```http
@ -194,7 +194,7 @@ PUT /api/v1/jobs/{id}
``` ```
| Field | Auth | Roles | Description | | Field | Auth | Roles | Description |
|-------|------|-------|-------------| |-------|------|-------|-------------|
| Public* | ❌ | companyAdmin, recruiter | Update job posting | | Public* | ❌ | admin, recruiter | Update job posting |
### Delete Job ### Delete Job
```http ```http
@ -202,7 +202,7 @@ DELETE /api/v1/jobs/{id}
``` ```
| Field | Auth | Roles | Description | | Field | Auth | Roles | Description |
|-------|------|-------|-------------| |-------|------|-------|-------------|
| Public* | ❌ | companyAdmin, recruiter | Delete job posting | | Public* | ❌ | admin, recruiter | Delete job posting |
### List Jobs for Moderation ### List Jobs for Moderation
```http ```http
@ -258,7 +258,7 @@ GET /api/v1/applications
``` ```
| Field | Auth | Roles | Description | | Field | Auth | Roles | Description |
|-------|------|-------|-------------| |-------|------|-------|-------------|
| Public | ❌ | companyAdmin, recruiter | List applications | | Public | ❌ | admin, recruiter | List applications |
### Get Application by ID ### Get Application by ID
```http ```http
@ -266,7 +266,7 @@ GET /api/v1/applications/{id}
``` ```
| Field | Auth | Roles | Description | | Field | Auth | Roles | Description |
|-------|------|-------|-------------| |-------|------|-------|-------------|
| Public | ❌ | companyAdmin, recruiter | Get application details | | Public | ❌ | admin, recruiter | Get application details |
### Update Application Status ### Update Application Status
```http ```http
@ -274,7 +274,7 @@ PUT /api/v1/applications/{id}/status
``` ```
| Field | Auth | Roles | Description | | Field | Auth | Roles | Description |
|-------|------|-------|-------------| |-------|------|-------|-------------|
| Public | ❌ | companyAdmin, recruiter | Update application status | | Public | ❌ | admin, recruiter | Update application status |
**Request:** **Request:**
```json ```json

View file

@ -27,8 +27,8 @@ This document details the security layers, authentication methods, and role-base
| Method | Route | Description | Notes | | Method | Route | Description | Notes |
| :--- | :--- | :--- | :--- | | :--- | :--- | :--- | :--- |
| `POST` | `/api/v1/auth/login` | User Login | Returns JWT + Cookie | | `POST` | `/api/v1/auth/login` | User Login | Returns JWT + Cookie |
| `POST` | `/api/v1/auth/register` | Candidate Register | Creates `jobSeeker` user | | `POST` | `/api/v1/auth/register` | Candidate Register | Creates `candidate` user |
| `POST` | `/api/v1/companies` | Company Register | Creates company + `companyAdmin` | | `POST` | `/api/v1/companies` | Company Register | Creates company + `admin` |
| `GET` | `/api/v1/jobs` | List Jobs | Public search/list | | `GET` | `/api/v1/jobs` | List Jobs | Public search/list |
| `GET` | `/api/v1/jobs/{id}` | Get Job | Public details | | `GET` | `/api/v1/jobs/{id}` | Get Job | Public details |
| `GET` | `/docs/*` | Swagger UI | API Documentation | | `GET` | `/docs/*` | Swagger UI | API Documentation |
@ -47,7 +47,7 @@ This document details the security layers, authentication methods, and role-base
| `DELETE` | `/api/v1/storage/files` | Delete S3 File | | `DELETE` | `/api/v1/storage/files` | Delete S3 File |
### 🟠 Recruiter / CompanyAdmin Routes ### 🟠 Recruiter / CompanyAdmin Routes
**Requirement**: Role `companyAdmin` OR `recruiter`. **Requirement**: Role `admin` OR `recruiter`.
| Method | Route | Description | | Method | Route | Description |
| :--- | :--- | :--- | | :--- | :--- | :--- |

View file

@ -156,7 +156,7 @@ CREATE TABLE users (
id SERIAL PRIMARY KEY, id SERIAL PRIMARY KEY,
identifier VARCHAR(100) UNIQUE NOT NULL, -- Login (username/email) identifier VARCHAR(100) UNIQUE NOT NULL, -- Login (username/email)
password_hash VARCHAR(255) NOT NULL, password_hash VARCHAR(255) NOT NULL,
role VARCHAR(20) NOT NULL, -- 'superadmin'|'companyAdmin'|'recruiter'|'jobSeeker' role VARCHAR(20) NOT NULL, -- 'superadmin'|'admin'|'recruiter'|'candidate'
-- Profile -- Profile
full_name VARCHAR(255), full_name VARCHAR(255),
@ -190,9 +190,9 @@ CREATE TABLE users (
**Roles:** **Roles:**
- `superadmin` - Platform administrator - `superadmin` - Platform administrator
- `companyAdmin` - Company administrator - `admin` - Company administrator
- `recruiter` - Job poster/recruiter - `recruiter` - Job poster/recruiter
- `jobSeeker` - Candidate/job seeker - `candidate` - Candidate/job seeker
--- ---
@ -544,7 +544,7 @@ CREATE TABLE user_companies (
id SERIAL PRIMARY KEY, id SERIAL PRIMARY KEY,
user_id INT NOT NULL REFERENCES users(id) ON DELETE CASCADE, user_id INT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
company_id INT NOT NULL REFERENCES companies(id) ON DELETE CASCADE, company_id INT NOT NULL REFERENCES companies(id) ON DELETE CASCADE,
role VARCHAR(30) DEFAULT 'recruiter', -- 'companyAdmin'|'recruiter' role VARCHAR(30) DEFAULT 'recruiter', -- 'admin'|'recruiter'
permissions JSONB, permissions JSONB,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE(user_id, company_id) UNIQUE(user_id, company_id)

View file

@ -33,11 +33,11 @@ export default function DashboardPage() {
return <AdminDashboardContent /> return <AdminDashboardContent />
} }
if (user.role === "company" || user.roles?.includes("companyAdmin")) { if (user.role === "company" || user.roles?.includes("admin")) {
return <CompanyDashboardContent user={user} /> return <CompanyDashboardContent user={user} />
} }
if (user.role === "candidate" || user.roles?.includes("jobSeeker")) { if (user.role === "candidate" || user.roles?.includes("candidate")) {
return <CandidateDashboardContent /> return <CandidateDashboardContent />
} }

View file

@ -45,7 +45,7 @@ export default function AdminUsersPage() {
name: "", name: "",
email: "", email: "",
password: "", password: "",
role: "jobSeeker", role: "candidate",
}) })
const [editFormData, setEditFormData] = useState({ const [editFormData, setEditFormData] = useState({
name: "", name: "",
@ -85,7 +85,7 @@ export default function AdminUsersPage() {
await usersApi.create(formData) await usersApi.create(formData)
toast.success("User created successfully!") toast.success("User created successfully!")
setIsDialogOpen(false) setIsDialogOpen(false)
setFormData({ name: "", email: "", password: "", role: "jobSeeker" }) setFormData({ name: "", email: "", password: "", role: "candidate" })
setPage(1) setPage(1)
loadUsers(1) loadUsers(1)
} catch (error) { } catch (error) {
@ -158,18 +158,16 @@ export default function AdminUsersPage() {
const getRoleBadge = (role: string) => { const getRoleBadge = (role: string) => {
const labels: Record<string, string> = { const labels: Record<string, string> = {
superadmin: "Super Admin", superadmin: "Super Admin",
companyAdmin: "Company admin", admin: "Company Admin",
recruiter: "Recruiter", recruiter: "Recruiter",
jobSeeker: "Candidate", candidate: "Candidate",
admin: "Admin",
company: "Company" company: "Company"
} }
const colors: Record<string, "default" | "secondary" | "destructive" | "outline"> = { const colors: Record<string, "default" | "secondary" | "destructive" | "outline"> = {
superadmin: "destructive", superadmin: "destructive",
companyAdmin: "default", admin: "default",
recruiter: "secondary", recruiter: "secondary",
jobSeeker: "outline", candidate: "outline",
admin: "destructive",
company: "default" company: "default"
} }
const label = labels[role] || role || "User" const label = labels[role] || role || "User"
@ -239,9 +237,9 @@ export default function AdminUsersPage() {
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
<SelectItem value="superadmin">Super Admin</SelectItem> <SelectItem value="superadmin">Super Admin</SelectItem>
<SelectItem value="companyAdmin">Company admin</SelectItem> <SelectItem value="admin">Company admin</SelectItem>
<SelectItem value="recruiter">Recruiter</SelectItem> <SelectItem value="recruiter">Recruiter</SelectItem>
<SelectItem value="jobSeeker">Candidate</SelectItem> <SelectItem value="candidate">Candidate</SelectItem>
</SelectContent> </SelectContent>
</Select> </Select>
</div> </div>
@ -307,7 +305,7 @@ export default function AdminUsersPage() {
<CardHeader className="pb-3"> <CardHeader className="pb-3">
<CardDescription>Admins (page)</CardDescription> <CardDescription>Admins (page)</CardDescription>
<CardTitle className="text-3xl"> <CardTitle className="text-3xl">
{users.filter((u) => u.role === "superadmin" || u.role === "companyAdmin").length} {users.filter((u) => u.role === "superadmin" || u.role === "admin" || u.role === "admin").length}
</CardTitle> </CardTitle>
</CardHeader> </CardHeader>
</Card> </Card>
@ -320,7 +318,7 @@ export default function AdminUsersPage() {
<Card> <Card>
<CardHeader className="pb-3"> <CardHeader className="pb-3">
<CardDescription>Candidates (page)</CardDescription> <CardDescription>Candidates (page)</CardDescription>
<CardTitle className="text-3xl">{users.filter((u) => u.role === "jobSeeker").length}</CardTitle> <CardTitle className="text-3xl">{users.filter((u) => u.role === "candidate" || u.role === "candidate").length}</CardTitle>
</CardHeader> </CardHeader>
</Card> </Card>
</div> </div>

View file

@ -44,9 +44,11 @@ export async function login(
// Note: The backend returns roles as an array of strings. The frontend expects a single 'role' or we need to adapt. // Note: The backend returns roles as an array of strings. The frontend expects a single 'role' or we need to adapt.
// For now we map the first role or main role to the 'role' field. // For now we map the first role or main role to the 'role' field.
let userRole: "candidate" | "admin" | "company" = "candidate"; let userRole: "candidate" | "admin" | "company" = "candidate";
if (data.user.roles.includes("superadmin") || data.user.roles.includes("admin") || data.user.roles.includes("ADMIN") || data.user.roles.includes("SUPERADMIN")) { // Check for SuperAdmin (Platform Admin)
if (data.user.roles.includes("superadmin") || data.user.roles.includes("SUPERADMIN")) {
userRole = "admin"; userRole = "admin";
} else if (data.user.roles.includes("companyAdmin") || data.user.roles.includes("recruiter")) { // Check for Company Admin (now called 'admin') or Recruiter
} else if (data.user.roles.includes("admin") || data.user.roles.includes("recruiter")) {
userRole = "company"; userRole = "company";
} }

View file

@ -14,19 +14,26 @@ async function resetDatabase() {
console.log('🗑️ Resetting database...'); console.log('🗑️ Resetting database...');
try { try {
// Drop all tables in reverse order (respecting foreign keys) // Dynamic drop: Fetch all tables in public schema and drop them
await pool.query('DROP TABLE IF EXISTS password_resets CASCADE'); // This avoids "must be owner of schema public" error by operating on tables directly
await pool.query('DROP TABLE IF EXISTS favorite_jobs CASCADE'); console.log('🔍 Finding tables to drop...');
await pool.query('DROP TABLE IF EXISTS applications CASCADE'); const tablesResult = await pool.query(`
await pool.query('DROP TABLE IF EXISTS jobs CASCADE'); SELECT tablename
await pool.query('DROP TABLE IF EXISTS user_companies CASCADE'); FROM pg_tables
await pool.query('DROP TABLE IF EXISTS companies CASCADE'); WHERE schemaname = 'public'
await pool.query('DROP TABLE IF EXISTS users CASCADE'); `);
await pool.query('DROP TABLE IF EXISTS cities CASCADE');
await pool.query('DROP TABLE IF EXISTS regions CASCADE');
await pool.query('DROP TABLE IF EXISTS prefectures CASCADE'); // Legacy drop
console.log('✅ All tables dropped successfully'); if (tablesResult.rows.length > 0) {
const tables = tablesResult.rows.map(row => row.tablename);
console.log(`🔥 Dropping ${tables.length} tables: ${tables.join(', ')}`);
// Construct a single DROP statement for all tables (CASCADE handles dependencies)
const tableList = tables.map(t => `"${t}"`).join(', ');
await pool.query(`DROP TABLE IF EXISTS ${tableList} CASCADE`);
console.log('✅ All public tables dropped successfully.');
} else {
console.log(' No tables found to drop IN public schema.');
}
} catch (error) { } catch (error) {
console.error('❌ Error resetting database:', error.message); console.error('❌ Error resetting database:', error.message);
throw error; throw error;

View file

@ -80,7 +80,12 @@ export async function seedJobs() {
let totalJobs = 0; let totalJobs = 0;
try { try {
// Process companies in chunks to avoid overwhelming the DB
for (const company of companies) { for (const company of companies) {
const jobValues = [];
const jobParams = [];
let paramCounter = 1;
// Generate 25 jobs per company // Generate 25 jobs per company
for (let i = 0; i < 25; i++) { for (let i = 0; i < 25; i++) {
const template = jobTemplates[i % jobTemplates.length]; const template = jobTemplates[i % jobTemplates.length];
@ -98,13 +103,8 @@ export async function seedJobs() {
const currency = currencies[i % currencies.length]; const currency = currencies[i % currencies.length];
const salaryType = salaryTypes[i % salaryTypes.length]; const salaryType = salaryTypes[i % salaryTypes.length];
// jobs.id is SERIAL - let DB auto-generate // Prepare params for bulk insert
await pool.query(` jobParams.push(
INSERT INTO jobs (company_id, created_by, title, description,
salary_min, salary_max, salary_type, currency, employment_type, working_hours,
location, requirements, benefits, visa_support, language_level, status, work_mode)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17)
`, [
company.id, company.id,
seedUserId, seedUserId,
title, title,
@ -122,11 +122,28 @@ export async function seedJobs() {
'beginner', 'beginner',
'open', 'open',
workMode workMode
]); );
// ($1, $2, $3, ...), ($18, $19, ...)
const placeHolders = [];
for (let k = 0; k < 17; k++) {
placeHolders.push(`$${paramCounter++}`);
}
jobValues.push(`(${placeHolders.join(',')})`);
totalJobs++; totalJobs++;
} }
// Bulk Insert for this company
if (jobValues.length > 0) {
const query = `
INSERT INTO jobs (company_id, created_by, title, description,
salary_min, salary_max, salary_type, currency, employment_type, working_hours,
location, requirements, benefits, visa_support, language_level, status, work_mode)
VALUES ${jobValues.join(',')}
`;
await pool.query(query, jobParams);
}
} }
console.log(`${totalJobs} jobs seeded across ${companies.length} companies`); console.log(`${totalJobs} jobs seeded across ${companies.length} companies`);

View file

@ -108,7 +108,7 @@ export async function seedNotifications() {
createdAt.setHours(createdAt.getHours() - hoursAgo); createdAt.setHours(createdAt.getHours() - hoursAgo);
await pool.query(` await pool.query(`
INSERT INTO notifications (id, user_id, title, message, type, is_read, created_at, updated_at) INSERT INTO notifications (id, user_id, title, message, type, read_at, created_at, updated_at)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
ON CONFLICT (id) DO NOTHING ON CONFLICT (id) DO NOTHING
`, [ `, [
@ -117,7 +117,7 @@ export async function seedNotifications() {
template.title, template.title,
template.message, template.message,
template.type, template.type,
template.is_read, template.is_read ? new Date() : null,
createdAt, createdAt,
createdAt createdAt
]); ]);

View file

@ -40,13 +40,13 @@ export async function seedUsers() {
console.log(' ✓ SuperAdmin created (superadmin)'); console.log(' ✓ SuperAdmin created (superadmin)');
// 2. Create Company Admins // 2. Create Company Admins
const companyAdmins = [ const admins = [
{ identifier: 'takeshi_yamamoto', fullName: 'Takeshi Yamamoto', company: 'TechCorp', email: 'takeshi@techcorp.com', pass: 'Takeshi@2025', roles: ['companyAdmin'] }, { identifier: 'takeshi_yamamoto', fullName: 'Takeshi Yamamoto', company: 'TechCorp', email: 'takeshi@techcorp.com', pass: 'Takeshi@2025', roles: ['admin'] },
{ identifier: 'kenji', fullName: 'Kenji Tanaka', company: 'AppMakers', email: 'kenji@appmakers.mobile', pass: 'Takeshi@2025', roles: ['companyAdmin'] }, { identifier: 'kenji', fullName: 'Kenji Tanaka', company: 'AppMakers', email: 'kenji@appmakers.mobile', pass: 'Takeshi@2025', roles: ['admin'] },
{ identifier: 'maria_santos', fullName: 'Maria Santos', company: 'DesignHub', email: 'maria@designhub.com', pass: 'User@2025', roles: ['recruiter'] } { identifier: 'maria_santos', fullName: 'Maria Santos', company: 'DesignHub', email: 'maria@designhub.com', pass: 'User@2025', roles: ['recruiter'] }
]; ];
for (const admin of companyAdmins) { for (const admin of admins) {
const hash = await bcrypt.hash(admin.pass + PASSWORD_PEPPER, 10); const hash = await bcrypt.hash(admin.pass + PASSWORD_PEPPER, 10);
const tenantId = companyMap[admin.company]; const tenantId = companyMap[admin.company];
@ -86,7 +86,7 @@ export async function seedUsers() {
const result = await pool.query(` const result = await pool.query(`
INSERT INTO users (identifier, password_hash, role, full_name, email, name, status) INSERT INTO users (identifier, password_hash, role, full_name, email, name, status)
VALUES ($1, $2, 'jobSeeker', $3, $4, $5, 'active') VALUES ($1, $2, 'candidate', $3, $4, $5, 'active')
ON CONFLICT (identifier) DO UPDATE SET password_hash = EXCLUDED.password_hash ON CONFLICT (identifier) DO UPDATE SET password_hash = EXCLUDED.password_hash
RETURNING id RETURNING id
`, [cand.identifier, hash, cand.fullName, cand.email, cand.fullName]); `, [cand.identifier, hash, cand.fullName, cand.email, cand.fullName]);
@ -178,7 +178,7 @@ export async function seedUsers() {
INSERT INTO users ( INSERT INTO users (
identifier, password_hash, role, full_name, email, name, phone, identifier, password_hash, role, full_name, email, name, phone,
city, state, title, experience, skills, objective, bio, status city, state, title, experience, skills, objective, bio, status
) VALUES ($1, $2, 'jobSeeker', $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, 'active') ) VALUES ($1, $2, 'candidate', $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, 'active')
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,