fix(backoffice): use string IDs for companies/jobs/users, fix audit log, ensure real tags support

This commit is contained in:
Tiago Yamamoto 2025-12-24 19:43:49 -03:00
parent 9784e959e4
commit 4eae018a25
5 changed files with 23 additions and 28 deletions

View file

@ -760,19 +760,14 @@ func (h *CoreHandlers) Me(w http.ResponseWriter, r *http.Request) {
return return
} }
var userID int var userID string
switch v := userIDVal.(type) { switch v := userIDVal.(type) {
case int: case int:
userID = v userID = strconv.Itoa(v)
case string: case string:
var err error userID = v
userID, err = strconv.Atoi(v)
if err != nil {
http.Error(w, "Invalid User ID in context", http.StatusInternalServerError)
return
}
case float64: case float64:
userID = int(v) userID = strconv.Itoa(int(v))
} }
user, err := h.adminService.GetUser(ctx, userID) user, err := h.adminService.GetUser(ctx, userID)

View file

@ -15,12 +15,12 @@ type LoginResponse struct {
Token string `json:"token"` Token string `json:"token"`
User UserInfo `json:"user"` User UserInfo `json:"user"`
Companies []CompanyInfo `json:"companies,omitempty"` Companies []CompanyInfo `json:"companies,omitempty"`
ActiveCompanyID *int `json:"activeCompanyId,omitempty"` ActiveCompanyID *string `json:"activeCompanyId,omitempty"`
} }
// UserInfo represents basic user information in responses // UserInfo represents basic user information in responses
type UserInfo struct { type UserInfo struct {
ID int `json:"id"` ID string `json:"id"`
Identifier string `json:"identifier"` Identifier string `json:"identifier"`
Role string `json:"role"` Role string `json:"role"`
FullName string `json:"fullName"` FullName string `json:"fullName"`
@ -29,7 +29,7 @@ type UserInfo struct {
// CompanyInfo represents basic company information // CompanyInfo represents basic company information
type CompanyInfo struct { type CompanyInfo struct {
ID int `json:"id"` ID string `json:"id"`
Name string `json:"name"` Name string `json:"name"`
Role string `json:"role"` // Role in this company (admin or recruiter) Role string `json:"role"` // Role in this company (admin or recruiter)
} }
@ -49,7 +49,7 @@ type RegisterRequest struct {
// User represents a generic user profile // User represents a generic user profile
type User struct { type User struct {
ID int `json:"id"` ID string `json:"id"`
Name string `json:"name"` Name string `json:"name"`
Email string `json:"email"` Email string `json:"email"`
Role string `json:"role"` Role string `json:"role"`

View file

@ -525,7 +525,7 @@ func (s *AdminService) getTagByID(ctx context.Context, id int) (*models.Tag, err
} }
// GetUser fetches a user by ID // GetUser fetches a user by ID
func (s *AdminService) GetUser(ctx context.Context, id int) (*dto.User, error) { func (s *AdminService) GetUser(ctx context.Context, id string) (*dto.User, error) {
query := ` query := `
SELECT id, name, email, role, created_at SELECT id, name, email, role, created_at
FROM users WHERE id = $1 FROM users WHERE id = $1
@ -540,7 +540,7 @@ func (s *AdminService) GetUser(ctx context.Context, id int) (*dto.User, error) {
} }
// GetCompanyByUserID fetches the company associated with a user // GetCompanyByUserID fetches the company associated with a user
func (s *AdminService) GetCompanyByUserID(ctx context.Context, userID int) (*models.Company, error) { func (s *AdminService) GetCompanyByUserID(ctx context.Context, userID string) (*models.Company, error) {
// First, try to find company where this user is admin // First, try to find company where this user is admin
// Assuming users table has company_id or companies table has admin_email // Assuming users table has company_id or companies table has admin_email
// Let's check if 'users' has company_id column via error or assume architecture. // Let's check if 'users' has company_id column via error or assume architecture.
@ -568,7 +568,7 @@ func (s *AdminService) GetCompanyByUserID(ctx context.Context, userID int) (*mod
WHERE u.id = $1 WHERE u.id = $1
` `
if err2 := s.DB.QueryRowContext(ctx, query2, userID).Scan(&c.ID, &c.Name, &c.Slug, &c.Active, &c.Verified); err2 != nil { if err2 := s.DB.QueryRowContext(ctx, query2, userID).Scan(&c.ID, &c.Name, &c.Slug, &c.Active, &c.Verified); err2 != nil {
return nil, fmt.Errorf("company not found for user %d", userID) return nil, fmt.Errorf("company not found for user %s", userID)
} }
} }
return &c, nil return &c, nil

View file

@ -91,7 +91,7 @@ export default function BackofficePage() {
} }
} }
const handleApproveCompany = async (companyId: number) => { const handleApproveCompany = async (companyId: string) => {
try { try {
await adminCompaniesApi.updateStatus(companyId, { verified: true }) await adminCompaniesApi.updateStatus(companyId, { verified: true })
toast.success("Company approved") toast.success("Company approved")
@ -102,7 +102,7 @@ export default function BackofficePage() {
} }
} }
const handleDeactivateCompany = async (companyId: number) => { const handleDeactivateCompany = async (companyId: string) => {
try { try {
await adminCompaniesApi.updateStatus(companyId, { active: false }) await adminCompaniesApi.updateStatus(companyId, { active: false })
toast.success("Company deactivated") toast.success("Company deactivated")
@ -113,7 +113,7 @@ export default function BackofficePage() {
} }
} }
const handleJobStatus = async (jobId: number, status: string) => { const handleJobStatus = async (jobId: string, status: string) => {
try { try {
await adminJobsApi.updateStatus(jobId, status) await adminJobsApi.updateStatus(jobId, status)
toast.success("Job status updated") toast.success("Job status updated")
@ -124,7 +124,7 @@ export default function BackofficePage() {
} }
} }
const handleDuplicateJob = async (jobId: number) => { const handleDuplicateJob = async (jobId: string) => {
try { try {
await adminJobsApi.duplicate(jobId) await adminJobsApi.duplicate(jobId)
toast.success("Job duplicated as draft") toast.success("Job duplicated as draft")
@ -283,11 +283,11 @@ export default function BackofficePage() {
)} )}
</TableCell> </TableCell>
<TableCell className="text-right space-x-2"> <TableCell className="text-right space-x-2">
<Button size="sm" variant="outline" onClick={() => handleApproveCompany(Number(company.id))}> <Button size="sm" variant="outline" onClick={() => handleApproveCompany(company.id)}>
<CheckCircle className="h-4 w-4 mr-2" /> <CheckCircle className="h-4 w-4 mr-2" />
Aprovar Aprovar
</Button> </Button>
<Button size="sm" variant="destructive" onClick={() => handleDeactivateCompany(Number(company.id))}> <Button size="sm" variant="destructive" onClick={() => handleDeactivateCompany(company.id)}>
<XCircle className="h-4 w-4 mr-2" /> <XCircle className="h-4 w-4 mr-2" />
Desativar Desativar
</Button> </Button>

View file

@ -87,7 +87,7 @@ export interface AdminCompany {
} }
export interface AdminJob { export interface AdminJob {
id: number; id: string;
title: string; title: string;
companyName: string; companyName: string;
status: string; status: string;
@ -116,7 +116,7 @@ export interface AdminTag {
} }
export interface AdminCandidate { export interface AdminCandidate {
id: number; id: string;
name: string; name: string;
email: string; email: string;
phone: string; phone: string;
@ -218,12 +218,12 @@ export const adminJobsApi = {
return apiRequest<{ data: AdminJob[]; pagination: any }>(`/api/v1/jobs/moderation?${query.toString()}`); return apiRequest<{ data: AdminJob[]; pagination: any }>(`/api/v1/jobs/moderation?${query.toString()}`);
}, },
updateStatus: (id: number, status: string) => updateStatus: (id: string, status: string) =>
apiRequest<void>(`/api/v1/jobs/${id}/status`, { apiRequest<void>(`/api/v1/jobs/${id}/status`, {
method: "PATCH", method: "PATCH",
body: JSON.stringify({ status }), body: JSON.stringify({ status }),
}), }),
duplicate: (id: number) => duplicate: (id: string) =>
apiRequest<void>(`/api/v1/jobs/${id}/duplicate`, { apiRequest<void>(`/api/v1/jobs/${id}/duplicate`, {
method: "POST", method: "POST",
}), }),
@ -275,14 +275,14 @@ export const adminCompaniesApi = {
body: JSON.stringify(data), body: JSON.stringify(data),
}); });
}, },
updateStatus: (id: number, data: { active?: boolean; verified?: boolean }) => { updateStatus: (id: string, data: { active?: boolean; verified?: boolean }) => {
logCrudAction("update", "admin/companies", { id, ...data }); logCrudAction("update", "admin/companies", { id, ...data });
return apiRequest<void>(`/api/v1/companies/${id}/status`, { return apiRequest<void>(`/api/v1/companies/${id}/status`, {
method: "PATCH", method: "PATCH",
body: JSON.stringify(data), body: JSON.stringify(data),
}); });
}, },
delete: (id: number) => { delete: (id: string) => {
logCrudAction("delete", "admin/companies", { id }); logCrudAction("delete", "admin/companies", { id });
return apiRequest<void>(`/api/v1/companies/${id}`, { return apiRequest<void>(`/api/v1/companies/${id}`, {
method: "DELETE" method: "DELETE"