Yimaru-BackEnd/internal/web_server/handlers/admin.go
Yared Yemane 2f73b60122 Allow ADMIN users to bulk deactivate and reactivate by role.
Platform ADMIN callers no longer hit 403 on these endpoints; bulk changes to platform users.role ADMIN remain restricted to SUPER_ADMIN, while team_members.team_role ADMIN is still eligible under path ADMIN.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-19 01:09:42 -07:00

636 lines
22 KiB
Go

package handlers
import (
"Yimaru-Backend/internal/domain"
"Yimaru-Backend/internal/services/authentication"
"Yimaru-Backend/internal/web_server/response"
"context"
"encoding/json"
"fmt"
"strconv"
"strings"
"time"
"github.com/gofiber/fiber/v2"
"go.uber.org/zap"
)
type CreateAdminReq struct {
FirstName string `json:"first_name" example:"John"`
LastName string `json:"last_name" example:"Doe"`
Email string `json:"email" example:"john.doe@example.com"`
PhoneNumber string `json:"phone_number" example:"1234567890"`
Password string `json:"password" example:"password123"`
}
// CreateAdmin godoc
// @Summary Create Admin
// @Description Create Admin
// @Tags admin
// @Accept json
// @Produce json
// @Param manger body CreateAdminReq true "Create admin"
// @Success 200 {object} response.APIResponse
// @Failure 400 {object} response.APIResponse
// @Failure 401 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse
// @Router /api/v1/admin [post]
func (h *Handler) CreateAdmin(c *fiber.Ctx) error {
// var OrganizationID domain.ValidInt64
var req CreateAdminReq
if err := c.BodyParser(&req); err != nil {
h.mongoLoggerSvc.Info("failed to parse CreateAdmin request",
zap.Int64("status_code", fiber.StatusBadRequest),
zap.Error(err),
zap.Time("timestamp", time.Now()),
)
return fiber.NewError(fiber.StatusBadRequest, "Invalid request:"+err.Error())
}
valErrs, ok := h.validator.Validate(c, req)
if !ok {
var errMsg string
for field, msg := range valErrs {
errMsg += fmt.Sprintf("%s: %s; ", field, msg)
}
h.mongoLoggerSvc.Error("validation failed for CreateAdmin request",
zap.Int64("status_code", fiber.StatusBadRequest),
zap.Any("validation_errors", valErrs),
zap.Time("timestamp", time.Now()),
)
return fiber.NewError(fiber.StatusBadRequest, errMsg)
}
// if req.OrganizationID == nil {
// OrganizationID = domain.ValidInt64{
// Value: 0,
// Valid: false,
// }
// } else {
// // _, err := h.companySvc.GetCompanyByID(c.Context(), *req.OrganizationID)
// // if err != nil {
// // h.mongoLoggerSvc.Error("invalid company ID for CreateAdmin",
// // zap.Int64("status_code", fiber.StatusInternalServerError),
// // zap.Int64("company_id", *req.OrganizationID),
// // zap.Error(err),
// // zap.Time("timestamp", time.Now()),
// // )
// // return fiber.NewError(fiber.StatusInternalServerError, "Company ID is invalid:"+err.Error())
// // }
// OrganizationID = domain.ValidInt64{
// Value: *req.OrganizationID,
// Valid: true,
// }
// }
user := domain.CreateUserReq{
FirstName: req.FirstName,
LastName: req.LastName,
Email: req.Email,
PhoneNumber: req.PhoneNumber,
Password: req.Password,
Role: string(domain.RoleAdmin),
}
newUser, err := h.userSvc.CreateUser(c.Context(), user, true)
if err != nil {
h.mongoLoggerSvc.Error("failed to create admin user",
zap.Int64("status_code", fiber.StatusInternalServerError),
zap.Any("request", req),
zap.Error(err),
zap.Time("timestamp", time.Now()),
)
return fiber.NewError(fiber.StatusInternalServerError, "Failed to create admin:"+err.Error())
}
// if req.OrganizationID != nil {
// _, err := h.companySvc.UpdateCompany(c.Context(), domain.UpdateCompany{
// ID: *req.OrganizationID,
// AdminID: domain.ValidInt64{
// Value: newUser.ID,
// Valid: true,
// },
// })
// if err != nil {
// h.mongoLoggerSvc.Error("failed to update company with new admin",
// zap.Int64("status_code", fiber.StatusInternalServerError),
// zap.Int64("company_id", *req.OrganizationID),
// zap.Int64("admin_id", newUser.ID),
// zap.Error(err),
// zap.Time("timestamp", time.Now()),
// )
// return fiber.NewError(fiber.StatusInternalServerError, "Failed to update company"+err.Error())
// }
// }
h.mongoLoggerSvc.Info("admin created successfully",
zap.Int64("admin_id", newUser.ID),
zap.String("email", newUser.Email),
zap.Time("timestamp", time.Now()),
)
actorID := c.Locals("user_id").(int64)
actorRole := string(c.Locals("role").(domain.Role))
ip := c.IP()
ua := c.Get("User-Agent")
meta, _ := json.Marshal(map[string]interface{}{"email": req.Email, "admin_id": newUser.ID})
go h.activityLogSvc.RecordAction(context.Background(), &actorID, &actorRole, domain.ActionAdminCreated, domain.ResourceAdmin, &newUser.ID, "Created admin: "+req.Email, meta, &ip, &ua)
h.sendInAppNotification(newUser.ID, domain.NOTIFICATION_TYPE_ADMIN_CREATED, "Welcome to Yimaru", "Your admin account has been created successfully.")
return response.WriteJSON(c, fiber.StatusOK, "Admin created successfully", nil, nil)
}
type AdminRes struct {
ID int64 `json:"id"`
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
Email string `json:"email"`
PhoneNumber string `json:"phone_number"`
Role domain.Role `json:"role"`
EmailVerified bool `json:"email_verified"`
PhoneVerified bool `json:"phone_verified"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
LastLogin time.Time `json:"last_login"`
SuspendedAt time.Time `json:"suspended_at"`
Suspended bool `json:"suspended"`
}
// GetAllAdmins godoc
// @Summary Get all Admins
// @Description Get all Admins
// @Tags admin
// @Accept json
// @Produce json
// @Param page query int false "Page number"
// @Param page_size query int false "Page size"
// @Success 200 {object} AdminRes
// @Failure 400 {object} response.APIResponse
// @Failure 401 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse
// @Router /api/v1/admin [get]
func (h *Handler) GetAllAdmins(c *fiber.Ctx) error {
searchQuery := c.Query("query")
searchString := domain.ValidString{
Value: searchQuery,
// Valid: searchQuery != "",
}
createdBeforeQuery := c.Query("created_before")
var createdBefore domain.ValidTime
if createdBeforeQuery != "" {
parsed, err := time.Parse(time.RFC3339, createdBeforeQuery)
if err != nil {
h.logger.Info("invalid created_before format", "error", err)
return fiber.NewError(fiber.StatusBadRequest, "Invalid created_before format")
}
createdBefore = domain.ValidTime{Value: parsed, Valid: true}
}
createdAfterQuery := c.Query("created_after")
var createdAfter domain.ValidTime
if createdAfterQuery != "" {
parsed, err := time.Parse(time.RFC3339, createdAfterQuery)
if err != nil {
h.logger.Info("invalid created_after format", "error", err)
return fiber.NewError(fiber.StatusBadRequest, "Invalid created_after format")
}
createdAfter = domain.ValidTime{Value: parsed, Valid: true}
}
// companyID := int64(c.QueryInt("company_id"))
filter := domain.UserFilter{
Role: string(domain.RoleAdmin),
// OrganizationID: domain.ValidInt64{
// Value: companyID,
// Valid: companyID != 0,
// },
Page: int64(c.QueryInt("page", 1) - 1),
PageSize: int64(c.QueryInt("page_size", 10)),
Query: searchString.Value,
CreatedBefore: createdBefore,
CreatedAfter: createdAfter,
}
if valErrs, ok := h.validator.Validate(c, filter); !ok {
var errMsg string
for f, msg := range valErrs {
errMsg += fmt.Sprintf("%s: %s; ", f, msg)
}
h.mongoLoggerSvc.Info("invalid filter values in GetAllAdmins request",
zap.Int("status_code", fiber.StatusBadRequest),
zap.Any("validation_errors", valErrs),
zap.Time("timestamp", time.Now()))
return fiber.NewError(fiber.StatusBadRequest, errMsg)
}
admins, total, err := h.userSvc.GetAllUsers(c.Context(), filter)
if err != nil {
h.mongoLoggerSvc.Error("failed to get admins",
zap.Int("status_code", fiber.StatusInternalServerError),
zap.Any("filter", filter),
zap.Error(err),
zap.Time("timestamp", time.Now()))
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get admins: "+err.Error())
}
result := make([]AdminRes, len(admins))
for i, admin := range admins {
lastLogin, err := h.authSvc.GetLastLogin(c.Context(), admin.ID)
if err != nil && err != authentication.ErrRefreshTokenNotFound {
h.mongoLoggerSvc.Error("failed to get last login",
zap.Int("status_code", fiber.StatusInternalServerError),
zap.Int64("admin_id", admin.ID),
zap.Error(err),
zap.Time("timestamp", time.Now()))
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get last login: "+err.Error())
}
if err == authentication.ErrRefreshTokenNotFound {
lastLogin = &admin.CreatedAt
}
result[i] = AdminRes{
ID: admin.ID,
FirstName: admin.FirstName,
LastName: admin.LastName,
Email: admin.Email,
PhoneNumber: admin.PhoneNumber,
Role: admin.Role,
EmailVerified: admin.EmailVerified,
PhoneVerified: admin.PhoneVerified,
CreatedAt: admin.CreatedAt,
LastLogin: *lastLogin,
}
}
h.mongoLoggerSvc.Info("admins retrieved successfully",
zap.Int("status_code", fiber.StatusOK),
zap.Int("count", len(result)),
zap.Int("page", int(filter.Page+1)),
zap.Time("timestamp", time.Now()),
)
return response.WritePaginatedJSON(c, fiber.StatusOK, "Admins retrieved successfully", result, nil, int(filter.Page+1), int(total))
}
// GetAdminByID godoc
// @Summary Get admin by id
// @Description Get a single admin by id
// @Tags admin
// @Accept json
// @Produce json
// @Param id path int true "User ID"
// @Success 200 {object} AdminRes
// @Failure 400 {object} response.APIResponse
// @Failure 401 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse
// @Router /api/v1/admin/{id} [get]
func (h *Handler) GetAdminByID(c *fiber.Ctx) error {
idStr := c.Params("id")
id, err := strconv.ParseInt(idStr, 10, 64)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid admin ID")
}
user, err := h.userSvc.GetUserByID(c.Context(), id)
if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get admin: "+err.Error())
}
lastLogin, err := h.authSvc.GetLastLogin(c.Context(), user.ID)
if err != nil && err != authentication.ErrRefreshTokenNotFound {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get last login: "+err.Error())
}
if err == authentication.ErrRefreshTokenNotFound {
lastLogin = &user.CreatedAt
}
res := AdminRes{
ID: user.ID,
FirstName: user.FirstName,
LastName: user.LastName,
Email: user.Email,
PhoneNumber: user.PhoneNumber,
Role: user.Role,
EmailVerified: user.EmailVerified,
PhoneVerified: user.PhoneVerified,
CreatedAt: user.CreatedAt,
LastLogin: *lastLogin,
}
return response.WriteJSON(c, fiber.StatusOK, "Admin retrieved successfully", res, nil)
}
type updateAdminReq struct {
FirstName string `json:"first_name" example:"John"`
LastName string `json:"last_name" example:"Doe"`
Suspended bool `json:"suspended" example:"false"`
}
// UpdateAdmin godoc
// @Summary Update Admin
// @Description Update Admin
// @Tags admin
// @Accept json
// @Produce json
// @Param admin body updateAdminReq true "Update Admin"
// @Success 200 {object} response.APIResponse
// @Failure 400 {object} response.APIResponse
// @Failure 401 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse
// @Router /api/v1/admin/{id} [put]
func (h *Handler) UpdateAdmin(c *fiber.Ctx) error {
var req updateAdminReq
if err := c.BodyParser(&req); err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body: "+err.Error())
}
adminIDStr := c.Params("id")
adminID, err := strconv.ParseInt(adminIDStr, 10, 64)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid admin ID")
}
// var orgID domain.ValidInt64
// if req.OrganizationID != nil {
// orgID = domain.ValidInt64{
// Value: *req.OrganizationID,
// Valid: true,
// }
// }
err = h.userSvc.UpdateUser(c.Context(), domain.UpdateUserReq{
UserID: adminID,
FirstName: req.FirstName,
LastName: req.LastName,
// OrganizationID: orgID,
})
if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to update admin: "+err.Error())
}
actorID := c.Locals("user_id").(int64)
actorRole := string(c.Locals("role").(domain.Role))
ip := c.IP()
ua := c.Get("User-Agent")
meta, _ := json.Marshal(map[string]interface{}{"admin_id": adminID, "first_name": req.FirstName, "last_name": req.LastName})
go h.activityLogSvc.RecordAction(context.Background(), &actorID, &actorRole, domain.ActionAdminUpdated, domain.ResourceAdmin, &adminID, fmt.Sprintf("Updated admin ID: %d", adminID), meta, &ip, &ua)
return response.WriteJSON(c, fiber.StatusOK, "Admin updated successfully", nil, nil)
}
// BulkDeactivateAccountsByRole godoc
// @Summary Bulk deactivate accounts by role (SUPER_ADMIN or ADMIN platform users only)
// @Description Sets all platform users with the given users.role to DEACTIVATED (except the caller) and all team_members with the given team_role to inactive. The path role must match a valid learner role or team role string (e.g. STUDENT, INSTRUCTOR, ADMIN, CONTENT_MANAGER). SUPER_ADMIN cannot be bulk-deactivated. ADMIN platform users must use SUPER_ADMIN to bulk change other platform ADMIN users (team_members with team_role ADMIN under path ADMIN remain allowed). Empty body allowed; optionally pass exclude_team_member_id to skip one team_members row (e.g. yourself).
// @Tags admin
// @Accept json
// @Produce json
// @Security Bearer
// @Param role path string true "Role key (matches users.role and/or team_members.team_role)"
// @Param body body domain.BulkAccountsByRoleRequest false "Optional exclusions"
// @Success 200 {object} domain.Response
// @Failure 400 {object} domain.ErrorResponse
// @Failure 403 {object} domain.ErrorResponse
// @Failure 500 {object} domain.ErrorResponse
// @Router /api/v1/admin/roles/{role}/bulk-deactivate [post]
func (h *Handler) BulkDeactivateAccountsByRole(c *fiber.Ctx) error {
callerRole, ok := c.Locals("role").(domain.Role)
if !ok {
return c.Status(fiber.StatusForbidden).JSON(domain.ErrorResponse{
Message: "Forbidden",
Error: "role not found in context",
})
}
if callerRole != domain.RoleSuperAdmin && callerRole != domain.RoleAdmin {
return c.Status(fiber.StatusForbidden).JSON(domain.ErrorResponse{
Message: "Forbidden",
Error: "only SUPER_ADMIN or ADMIN platform users may bulk deactivate by role",
})
}
actorID, ok := c.Locals("user_id").(int64)
if !ok || actorID <= 0 {
return c.Status(fiber.StatusUnauthorized).JSON(domain.ErrorResponse{
Message: "Unauthorized",
Error: "user id not found in context",
})
}
roleKey := strings.ToUpper(strings.TrimSpace(c.Params("role")))
if roleKey == "" {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "Invalid role",
Error: "role path parameter is required",
})
}
if roleKey == string(domain.RoleSuperAdmin) || roleKey == string(domain.TeamRoleSuperAdmin) {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "Refusing bulk deactivate",
Error: "SUPER_ADMIN cannot be bulk deactivated",
})
}
validUserRole := domain.Role(roleKey).IsValid()
validTeamRole := domain.TeamRole(roleKey).IsValid()
if !validUserRole && !validTeamRole {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "Invalid role",
Error: "role is not a valid platform users.role nor team_members.team_role",
})
}
// Non-super-admins cannot bulk change other platform ADMIN users (same role); team_members ADMIN is still allowed.
if callerRole != domain.RoleSuperAdmin && roleKey == string(domain.RoleAdmin) {
validUserRole = false
}
var req domain.BulkAccountsByRoleRequest
if len(c.Body()) > 0 {
if err := c.BodyParser(&req); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "Invalid body",
Error: err.Error(),
})
}
}
if req.ExcludeTeamMemberID != nil && *req.ExcludeTeamMemberID <= 0 {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "Invalid exclude_team_member_id",
Error: "exclude_team_member_id must be positive when set",
})
}
var usersN, teamN int64
var err error
if validUserRole {
usersN, err = h.userSvc.BulkDeactivateUsersByRole(c.Context(), roleKey, actorID)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
Message: "Bulk user deactivation failed",
Error: err.Error(),
})
}
}
if validTeamRole {
teamN, err = h.teamSvc.BulkDeactivateTeamMembersByRole(c.Context(), roleKey, req.ExcludeTeamMemberID)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
Message: "Bulk team member deactivation failed",
Error: err.Error(),
})
}
}
out := domain.BulkDeactivateAccountsByRoleResult{
Role: roleKey,
UsersDeactivated: usersN,
TeamMembersDeactivated: teamN,
}
actorRole := string(callerRole)
ip := c.IP()
ua := c.Get("User-Agent")
meta, _ := json.Marshal(map[string]interface{}{
"role": roleKey,
"users_deactivated": usersN,
"team_members_deactivated": teamN,
"exclude_team_member_id": req.ExcludeTeamMemberID,
})
go h.activityLogSvc.RecordAction(context.Background(), &actorID, &actorRole, domain.ActionUserUpdated, domain.ResourceUser, &actorID, fmt.Sprintf("Bulk deactivated role %s (%d users, %d team members)", roleKey, usersN, teamN), meta, &ip, &ua)
return c.JSON(domain.Response{
Message: "Bulk deactivation completed",
Data: out,
Success: true,
StatusCode: fiber.StatusOK,
})
}
// BulkReactivateAccountsByRole godoc
// @Summary Bulk reactivate accounts by role (SUPER_ADMIN or ADMIN platform users only)
// @Description Sets all platform users with the given role from DEACTIVATED to ACTIVE (except the caller) and all team_members with the given team_role from inactive to active. Path role must be a valid platform or team role. SUPER_ADMIN cannot be bulk changed. ADMIN callers cannot bulk change other platform ADMIN users (team_members ADMIN under path ADMIN is allowed). Matches only users currently DEACTIVATED and team rows currently inactive.
// @Tags admin
// @Accept json
// @Produce json
// @Security Bearer
// @Param role path string true "Role key (matches users.role and/or team_members.team_role)"
// @Param body body domain.BulkAccountsByRoleRequest false "Optional exclusions"
// @Success 200 {object} domain.Response
// @Failure 400 {object} domain.ErrorResponse
// @Failure 403 {object} domain.ErrorResponse
// @Failure 500 {object} domain.ErrorResponse
// @Router /api/v1/admin/roles/{role}/bulk-reactivate [post]
func (h *Handler) BulkReactivateAccountsByRole(c *fiber.Ctx) error {
callerRole, ok := c.Locals("role").(domain.Role)
if !ok {
return c.Status(fiber.StatusForbidden).JSON(domain.ErrorResponse{
Message: "Forbidden",
Error: "role not found in context",
})
}
if callerRole != domain.RoleSuperAdmin && callerRole != domain.RoleAdmin {
return c.Status(fiber.StatusForbidden).JSON(domain.ErrorResponse{
Message: "Forbidden",
Error: "only SUPER_ADMIN or ADMIN platform users may bulk reactivate by role",
})
}
actorID, ok := c.Locals("user_id").(int64)
if !ok || actorID <= 0 {
return c.Status(fiber.StatusUnauthorized).JSON(domain.ErrorResponse{
Message: "Unauthorized",
Error: "user id not found in context",
})
}
roleKey := strings.ToUpper(strings.TrimSpace(c.Params("role")))
if roleKey == "" {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "Invalid role",
Error: "role path parameter is required",
})
}
if roleKey == string(domain.RoleSuperAdmin) || roleKey == string(domain.TeamRoleSuperAdmin) {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "Refusing bulk reactivate",
Error: "SUPER_ADMIN role cannot be bulk reactivated via this endpoint",
})
}
validUserRole := domain.Role(roleKey).IsValid()
validTeamRole := domain.TeamRole(roleKey).IsValid()
if !validUserRole && !validTeamRole {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "Invalid role",
Error: "role is not a valid platform users.role nor team_members.team_role",
})
}
// Non-super-admins cannot bulk change other platform ADMIN users; team_members ADMIN is still allowed.
if callerRole != domain.RoleSuperAdmin && roleKey == string(domain.RoleAdmin) {
validUserRole = false
}
var req domain.BulkAccountsByRoleRequest
if len(c.Body()) > 0 {
if err := c.BodyParser(&req); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "Invalid body",
Error: err.Error(),
})
}
}
if req.ExcludeTeamMemberID != nil && *req.ExcludeTeamMemberID <= 0 {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "Invalid exclude_team_member_id",
Error: "exclude_team_member_id must be positive when set",
})
}
var usersN, teamN int64
var err error
if validUserRole {
usersN, err = h.userSvc.BulkReactivateUsersByRole(c.Context(), roleKey, actorID)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
Message: "Bulk user reactivation failed",
Error: err.Error(),
})
}
}
if validTeamRole {
teamN, err = h.teamSvc.BulkReactivateTeamMembersByRole(c.Context(), roleKey, req.ExcludeTeamMemberID)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
Message: "Bulk team member reactivation failed",
Error: err.Error(),
})
}
}
out := domain.BulkReactivateAccountsByRoleResult{
Role: roleKey,
UsersReactivated: usersN,
TeamMembersReactivated: teamN,
}
actorRoleStr := string(callerRole)
ip := c.IP()
ua := c.Get("User-Agent")
meta, _ := json.Marshal(map[string]interface{}{
"role": roleKey,
"users_reactivated": usersN,
"team_members_reactivated": teamN,
"exclude_team_member_id": req.ExcludeTeamMemberID,
})
go h.activityLogSvc.RecordAction(context.Background(), &actorID, &actorRoleStr, domain.ActionUserUpdated, domain.ResourceUser, &actorID, fmt.Sprintf("Bulk reactivated role %s (%d users, %d team members)", roleKey, usersN, teamN), meta, &ip, &ua)
return c.JSON(domain.Response{
Message: "Bulk reactivation completed",
Data: out,
Success: true,
StatusCode: fiber.StatusOK,
})
}