fix(users): resolve 403 on update and enable role/status editing

This commit is contained in:
Tiago Yamamoto 2025-12-26 01:14:18 -03:00
parent 3d7612901d
commit 6ab7e357fb
6 changed files with 136 additions and 13 deletions

View file

@ -394,8 +394,19 @@ func (h *CoreHandlers) DeleteUser(w http.ResponseWriter, r *http.Request) {
// @Router /api/v1/users/{id} [patch]
func (h *CoreHandlers) UpdateUser(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
tenantID, ok := ctx.Value(middleware.ContextTenantID).(string)
if !ok || tenantID == "" {
tenantID, _ := ctx.Value(middleware.ContextTenantID).(string)
// Check for admin role to bypass tenant check
userRoles := middleware.ExtractRoles(ctx.Value(middleware.ContextRoles))
isAdmin := false
for _, role := range userRoles {
if role == "ADMIN" || role == "SUPERADMIN" || role == "admin" || role == "superadmin" {
isAdmin = true
break
}
}
if !isAdmin && tenantID == "" {
http.Error(w, "Tenant ID not found in context", http.StatusForbidden)
return
}

View file

@ -20,9 +20,11 @@ type CreateUserRequest struct {
}
type UpdateUserRequest struct {
Name *string `json:"name,omitempty"`
Email *string `json:"email,omitempty"`
Active *bool `json:"active,omitempty"`
Name *string `json:"name,omitempty"`
Email *string `json:"email,omitempty"`
Active *bool `json:"active,omitempty"`
Status *string `json:"status,omitempty"`
Roles *[]string `json:"roles,omitempty"`
}
type UserResponse struct {

View file

@ -4,6 +4,7 @@ import (
"context"
"errors"
"github.com/rede5/gohorsejobs/backend/internal/core/domain/entity"
"github.com/rede5/gohorsejobs/backend/internal/core/dto"
"github.com/rede5/gohorsejobs/backend/internal/core/ports"
)
@ -29,7 +30,8 @@ func (uc *UpdateUserUseCase) Execute(ctx context.Context, id, tenantID string, i
}
// 2. Check Permission (Tenant Check)
if user.TenantID != tenantID {
// If tenantID is empty, it means SuperAdmin or Admin (handler logic), so we skip check.
if tenantID != "" && user.TenantID != tenantID {
return nil, errors.New("forbidden: user belongs to another tenant")
}
@ -40,11 +42,20 @@ func (uc *UpdateUserUseCase) Execute(ctx context.Context, id, tenantID string, i
if input.Email != nil {
user.Email = *input.Email
}
if input.Active != nil {
if input.Status != nil {
user.Status = *input.Status
} else if input.Active != nil {
if *input.Active {
user.Status = "ACTIVE"
user.Status = "active"
} else {
user.Status = "INACTIVE"
user.Status = "inactive"
}
}
if input.Roles != nil {
user.Roles = []entity.Role{}
for _, r := range *input.Roles {
user.Roles = append(user.Roles, entity.Role{Name: r})
}
}

View file

@ -151,9 +151,51 @@ func (r *UserRepository) FindAllByTenant(ctx context.Context, tenantID string, l
func (r *UserRepository) Update(ctx context.Context, user *entity.User) (*entity.User, error) {
user.UpdatedAt = time.Now()
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)
return user, err
tx, err := r.db.BeginTx(ctx, nil)
if err != nil {
return nil, err
}
defer tx.Rollback()
// 1. Update basic fields + legacy role column
// We use the first role as the "legacy" role for compatibility
primaryRole := ""
if len(user.Roles) > 0 {
primaryRole = user.Roles[0].Name
}
query := `UPDATE users SET name=$1, email=$2, status=$3, role=$4, updated_at=$5 WHERE id=$6`
_, err = tx.ExecContext(ctx, query, user.Name, user.Email, user.Status, primaryRole, user.UpdatedAt, user.ID)
if err != nil {
return nil, err
}
// 2. Update user_roles (Delete all and re-insert)
_, err = tx.ExecContext(ctx, `DELETE FROM user_roles WHERE user_id=$1`, user.ID)
if err != nil {
return nil, err
}
if len(user.Roles) > 0 {
stmt, err := tx.PrepareContext(ctx, `INSERT INTO user_roles (user_id, role) VALUES ($1, $2)`)
if err != nil {
return nil, err
}
defer stmt.Close()
for _, role := range user.Roles {
if _, err := stmt.ExecContext(ctx, user.ID, role.Name); err != nil {
return nil, err
}
}
}
if err := tx.Commit(); err != nil {
return nil, err
}
return user, nil
}
func (r *UserRepository) Delete(ctx context.Context, id string) error {

View file

@ -50,6 +50,8 @@ export default function AdminUsersPage() {
const [editFormData, setEditFormData] = useState({
name: "",
email: "",
role: "",
status: "",
})
useEffect(() => {
@ -101,6 +103,8 @@ export default function AdminUsersPage() {
setEditFormData({
name: user.name,
email: user.email,
role: user.role,
status: user.status || "active",
})
setIsEditDialogOpen(true)
}
@ -109,7 +113,11 @@ export default function AdminUsersPage() {
if (!selectedUser) return
try {
setUpdating(true)
await usersApi.update(selectedUser.id, editFormData)
const payload = {
...editFormData,
roles: [editFormData.role], // Backend expects array of roles
}
await usersApi.update(selectedUser.id, payload)
toast.success("User updated successfully!")
setIsEditDialogOpen(false)
loadUsers() // Refresh list
@ -280,6 +288,32 @@ export default function AdminUsersPage() {
placeholder="email@example.com"
/>
</div>
<div className="grid gap-2">
<Label htmlFor="edit-role">Role</Label>
<Select value={editFormData.role} onValueChange={(v) => setEditFormData({ ...editFormData, role: v })}>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="superadmin">Super Admin</SelectItem>
<SelectItem value="admin">Company admin</SelectItem>
<SelectItem value="recruiter">Recruiter</SelectItem>
<SelectItem value="candidate">Candidate</SelectItem>
</SelectContent>
</Select>
</div>
<div className="grid gap-2">
<Label htmlFor="edit-status">Status</Label>
<Select value={editFormData.status} onValueChange={(v) => setEditFormData({ ...editFormData, status: v })}>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="active">Active</SelectItem>
<SelectItem value="inactive">Inactive</SelectItem>
</SelectContent>
</Select>
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setIsEditDialogOpen(false)}>Cancel</Button>

View file

@ -0,0 +1,23 @@
import { pool } from './db.js';
async function verifyTags() {
console.log('🔍 Checking job_tags table...');
try {
const res = await pool.query('SELECT category, COUNT(*) as count FROM job_tags GROUP BY category');
if (res.rows.length === 0) {
console.log('⚠️ No tags found in database.');
} else {
console.log('✅ Tags found:');
console.table(res.rows);
const total = await pool.query('SELECT COUNT(*) FROM job_tags');
console.log(`Total tags: ${total.rows[0].count}`);
}
} catch (err) {
console.error('❌ Error checking tags:', err.message);
} finally {
await pool.end();
}
}
verifyTags();