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 |
|--------|----------|-------|-----------|
| `GET` | `/api/v1/users` | `superadmin`, `companyAdmin` | Listar usuários |
| `POST` | `/api/v1/users` | `superadmin`, `companyAdmin` | Criar usuário |
| `GET` | `/api/v1/users` | `superadmin`, `admin` | Listar usuários |
| `POST` | `/api/v1/users` | `superadmin`, `admin` | Criar 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"
// 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).
type User struct {
ID string `json:"id"`

View file

@ -31,7 +31,7 @@ type UserInfo struct {
type CompanyInfo struct {
ID int `json:"id"`
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
@ -44,7 +44,7 @@ type RegisterRequest struct {
LineID *string `json:"lineId,omitempty"`
Instagram *string `json:"instagram,omitempty"`
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

View file

@ -99,7 +99,7 @@ type UpdateCompanyRequest struct {
type AssignUserToCompanyRequest struct {
UserID string `json:"userId" 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"`
}

View file

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

View file

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

View file

@ -7,7 +7,7 @@ type User struct {
ID string `json:"id" db:"id"`
Identifier string `json:"identifier" db:"identifier"`
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
FullName string `json:"fullName" db:"full_name"`

View file

@ -11,7 +11,7 @@ type UserCompany struct {
ID string `json:"id" db:"id"`
UserID string `json:"userId" db:"user_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"`
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 := `
SELECT id, full_name, email, phone, city, state, title, experience, bio, skills, avatar_url, created_at
FROM users
WHERE role = 'jobSeeker'
WHERE role = 'candidate'
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(),
identifier VARCHAR(100) UNIQUE 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
full_name VARCHAR(255),
@ -32,4 +32,4 @@ CREATE INDEX idx_users_active ON users(active);
-- Comments for documentation
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.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(),
user_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
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
@ -25,5 +25,5 @@ CREATE INDEX idx_user_companies_role ON user_companies(role);
-- Comments
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';

View file

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

View file

@ -1,6 +1,3 @@
DROP TABLE IF EXISTS ticket_messages;
DROP TABLE IF EXISTS tickets;
CREATE TABLE tickets (
id UUID PRIMARY KEY DEFAULT uuid_generate_v7(),
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 |
|------|------|-------|-------------|
| **SuperAdmin** | `superadmin` | 0 | Platform administrator |
| **CompanyAdmin** | `companyAdmin` | 1 | Company administrator |
| **Admin** | `admin` | 1 | Company administrator |
| **Recruiter** | `recruiter` | 2 | Job poster |
| **JobSeeker** | `jobSeeker` | 3 | Candidate |
| **Candidate** | `candidate` | 3 | Candidate |
| **Guest** | - | - | No authentication |
---
@ -124,7 +124,7 @@ POST /api/v1/users
```
| Field | Auth | Roles | Description |
|-------|------|-------|-------------|
| Protected | ✅ | superadmin, companyAdmin | Create new user |
| Protected | ✅ | superadmin, admin | Create new user |
### Update User
```http
@ -186,7 +186,7 @@ POST /api/v1/jobs
```
| Field | Auth | Roles | Description |
|-------|------|-------|-------------|
| Public* | ❌ | companyAdmin, recruiter | Create new job posting |
| Public* | ❌ | admin, recruiter | Create new job posting |
### Update Job
```http
@ -194,7 +194,7 @@ PUT /api/v1/jobs/{id}
```
| Field | Auth | Roles | Description |
|-------|------|-------|-------------|
| Public* | ❌ | companyAdmin, recruiter | Update job posting |
| Public* | ❌ | admin, recruiter | Update job posting |
### Delete Job
```http
@ -202,7 +202,7 @@ DELETE /api/v1/jobs/{id}
```
| Field | Auth | Roles | Description |
|-------|------|-------|-------------|
| Public* | ❌ | companyAdmin, recruiter | Delete job posting |
| Public* | ❌ | admin, recruiter | Delete job posting |
### List Jobs for Moderation
```http
@ -258,7 +258,7 @@ GET /api/v1/applications
```
| Field | Auth | Roles | Description |
|-------|------|-------|-------------|
| Public | ❌ | companyAdmin, recruiter | List applications |
| Public | ❌ | admin, recruiter | List applications |
### Get Application by ID
```http
@ -266,7 +266,7 @@ GET /api/v1/applications/{id}
```
| Field | Auth | Roles | Description |
|-------|------|-------|-------------|
| Public | ❌ | companyAdmin, recruiter | Get application details |
| Public | ❌ | admin, recruiter | Get application details |
### Update Application Status
```http
@ -274,7 +274,7 @@ PUT /api/v1/applications/{id}/status
```
| Field | Auth | Roles | Description |
|-------|------|-------|-------------|
| Public | ❌ | companyAdmin, recruiter | Update application status |
| Public | ❌ | admin, recruiter | Update application status |
**Request:**
```json

View file

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

View file

@ -156,7 +156,7 @@ CREATE TABLE users (
id SERIAL PRIMARY KEY,
identifier VARCHAR(100) UNIQUE NOT NULL, -- Login (username/email)
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
full_name VARCHAR(255),
@ -190,9 +190,9 @@ CREATE TABLE users (
**Roles:**
- `superadmin` - Platform administrator
- `companyAdmin` - Company administrator
- `admin` - Company administrator
- `recruiter` - Job poster/recruiter
- `jobSeeker` - Candidate/job seeker
- `candidate` - Candidate/job seeker
---
@ -544,7 +544,7 @@ CREATE TABLE user_companies (
id SERIAL PRIMARY KEY,
user_id INT NOT NULL REFERENCES users(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,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE(user_id, company_id)

View file

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

View file

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

View file

@ -14,19 +14,26 @@ async function resetDatabase() {
console.log('🗑️ Resetting database...');
try {
// Drop all tables in reverse order (respecting foreign keys)
await pool.query('DROP TABLE IF EXISTS password_resets CASCADE');
await pool.query('DROP TABLE IF EXISTS favorite_jobs CASCADE');
await pool.query('DROP TABLE IF EXISTS applications CASCADE');
await pool.query('DROP TABLE IF EXISTS jobs CASCADE');
await pool.query('DROP TABLE IF EXISTS user_companies CASCADE');
await pool.query('DROP TABLE IF EXISTS companies CASCADE');
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
// Dynamic drop: Fetch all tables in public schema and drop them
// This avoids "must be owner of schema public" error by operating on tables directly
console.log('🔍 Finding tables to drop...');
const tablesResult = await pool.query(`
SELECT tablename
FROM pg_tables
WHERE schemaname = 'public'
`);
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) {
console.error('❌ Error resetting database:', error.message);
throw error;

View file

@ -80,7 +80,12 @@ export async function seedJobs() {
let totalJobs = 0;
try {
// Process companies in chunks to avoid overwhelming the DB
for (const company of companies) {
const jobValues = [];
const jobParams = [];
let paramCounter = 1;
// Generate 25 jobs per company
for (let i = 0; i < 25; i++) {
const template = jobTemplates[i % jobTemplates.length];
@ -98,13 +103,8 @@ export async function seedJobs() {
const currency = currencies[i % currencies.length];
const salaryType = salaryTypes[i % salaryTypes.length];
// jobs.id is SERIAL - let DB auto-generate
await pool.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 ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17)
`, [
// Prepare params for bulk insert
jobParams.push(
company.id,
seedUserId,
title,
@ -122,11 +122,28 @@ export async function seedJobs() {
'beginner',
'open',
workMode
]);
);
// ($1, $2, $3, ...), ($18, $19, ...)
const placeHolders = [];
for (let k = 0; k < 17; k++) {
placeHolders.push(`$${paramCounter++}`);
}
jobValues.push(`(${placeHolders.join(',')})`);
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`);

View file

@ -108,7 +108,7 @@ export async function seedNotifications() {
createdAt.setHours(createdAt.getHours() - hoursAgo);
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)
ON CONFLICT (id) DO NOTHING
`, [
@ -117,7 +117,7 @@ export async function seedNotifications() {
template.title,
template.message,
template.type,
template.is_read,
template.is_read ? new Date() : null,
createdAt,
createdAt
]);

View file

@ -40,13 +40,13 @@ export async function seedUsers() {
console.log(' ✓ SuperAdmin created (superadmin)');
// 2. Create Company Admins
const companyAdmins = [
{ identifier: 'takeshi_yamamoto', fullName: 'Takeshi Yamamoto', company: 'TechCorp', email: 'takeshi@techcorp.com', pass: 'Takeshi@2025', roles: ['companyAdmin'] },
{ identifier: 'kenji', fullName: 'Kenji Tanaka', company: 'AppMakers', email: 'kenji@appmakers.mobile', pass: 'Takeshi@2025', roles: ['companyAdmin'] },
const admins = [
{ 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: ['admin'] },
{ 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 tenantId = companyMap[admin.company];
@ -86,7 +86,7 @@ export async function seedUsers() {
const result = await pool.query(`
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
RETURNING id
`, [cand.identifier, hash, cand.fullName, cand.email, cand.fullName]);
@ -178,7 +178,7 @@ export async function seedUsers() {
INSERT INTO users (
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')
) VALUES ($1, $2, 'candidate', $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,