diff --git a/backend/internal/api/handlers/core_handlers.go b/backend/internal/api/handlers/core_handlers.go index 633b8b6..68a3771 100644 --- a/backend/internal/api/handlers/core_handlers.go +++ b/backend/internal/api/handlers/core_handlers.go @@ -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 } diff --git a/backend/internal/core/dto/user_auth.go b/backend/internal/core/dto/user_auth.go index 361460e..818a4ab 100644 --- a/backend/internal/core/dto/user_auth.go +++ b/backend/internal/core/dto/user_auth.go @@ -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 { diff --git a/backend/internal/core/usecases/user/update_user.go b/backend/internal/core/usecases/user/update_user.go index f5d0f83..5d8ba88 100644 --- a/backend/internal/core/usecases/user/update_user.go +++ b/backend/internal/core/usecases/user/update_user.go @@ -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}) } } diff --git a/backend/internal/infrastructure/persistence/postgres/user_repository.go b/backend/internal/infrastructure/persistence/postgres/user_repository.go index 17b9e2e..851d871 100644 --- a/backend/internal/infrastructure/persistence/postgres/user_repository.go +++ b/backend/internal/infrastructure/persistence/postgres/user_repository.go @@ -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 { diff --git a/frontend/src/app/dashboard/users/page.tsx b/frontend/src/app/dashboard/users/page.tsx index d8d2505..10aa0c1 100644 --- a/frontend/src/app/dashboard/users/page.tsx +++ b/frontend/src/app/dashboard/users/page.tsx @@ -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" /> +
+ + +
+
+ + +
diff --git a/seeder-api/src/verify-tags.js b/seeder-api/src/verify-tags.js new file mode 100644 index 0000000..9ac9516 --- /dev/null +++ b/seeder-api/src/verify-tags.js @@ -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();