fix(users): resolve 403 on update and enable role/status editing
This commit is contained in:
parent
3d7612901d
commit
6ab7e357fb
6 changed files with 136 additions and 13 deletions
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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})
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
23
seeder-api/src/verify-tags.js
Normal file
23
seeder-api/src/verify-tags.js
Normal 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();
|
||||
Loading…
Reference in a new issue