package handler import ( "context" "log/slog" "os" "strings" "net/url" "github.com/descope/go-sdk/descope" "github.com/descope/go-sdk/descope/client" "github.com/gofiber/fiber/v2" ) type AdminHandler struct { DescopeClient *client.DescopeClient } func NewAdminHandler() *AdminHandler { projectID := os.Getenv("DESCOPE_PROJECT_ID") managementKey := os.Getenv("DESCOPE_MANAGEMENT_KEY") var descopeClient *client.DescopeClient var err error if projectID != "" && managementKey != "" { descopeClient, err = client.NewWithConfig(&client.Config{ ProjectID: projectID, ManagementKey: managementKey, }) if err != nil { slog.Warn("Failed to initialize Descope Client for Admin", "error", err) } } else { slog.Warn("DESCOPE_PROJECT_ID or DESCOPE_MANAGEMENT_KEY missing. Admin functions will fail.") } return &AdminHandler{ DescopeClient: descopeClient, } } func boolPtr(b bool) *bool { return &b } // checkAuth Helper func (h *AdminHandler) checkAuth(c *fiber.Ctx) error { adminPass := os.Getenv("ADMIN_PASSWORD") if adminPass == "" { adminPass = "admin" // Default fallback } reqPass := c.Get("X-Admin-Password") if reqPass != adminPass { return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "Invalid Admin Password"}) } return nil } type CreateUserRequest struct { LoginID string `json:"loginId"` Email string `json:"email"` Phone string `json:"phone"` DisplayName string `json:"displayName"` Roles []string `json:"roles"` Tenants map[string][]string `json:"tenants"` // tenantId -> roles } func (h *AdminHandler) CheckAuth(c *fiber.Ctx) error { if err := h.checkAuth(c); err != nil { return err } return c.Status(fiber.StatusOK).JSON(fiber.Map{"status": "ok"}) } // ListUsers - GET /api/v1/admin/users func (h *AdminHandler) ListUsers(c *fiber.Ctx) error { if err := h.checkAuth(c); err != nil { return err } text := c.Query("text") // Limit is not directly supported in SearchAll options as a simple int in all SDK versions, // but let's check the options struct. // Based on previous inspection: SearchAll takes UserSearchOptions. var users []*descope.UserResponse var err error if text != "" { options := &descope.UserSearchOptions{ Text: text, Limit: 50 } users, _, err = h.DescopeClient.Management.User().SearchAll(context.Background(), options) } else { // Nil options means default search (usually returns all or default page) users, _, err = h.DescopeClient.Management.User().SearchAll(context.Background(), nil) } if err != nil { slog.Error("[Admin] ListUsers failed", "error", err) return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) } return c.JSON(fiber.Map{"users": users}) } // DeleteUser - DELETE /api/v1/admin/users/:loginId func (h *AdminHandler) DeleteUser(c *fiber.Ctx) error { if err := h.checkAuth(c); err != nil { return err } loginID := c.Params("loginId") // Decode if necessary (Fiber usually decodes params, but let's be safe if it's double encoded) if decoded, err := url.QueryUnescape(loginID); err == nil { loginID = decoded } slog.Info("[Admin] Deleting user", "loginID", loginID) if err := h.DescopeClient.Management.User().Delete(context.Background(), loginID); err != nil { slog.Error("[Admin] DeleteUser failed", "error", err) return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) } return c.JSON(fiber.Map{"message": "User deleted successfully"}) } // UpdateUserStatus - PATCH /api/v1/admin/users/:loginId/status func (h *AdminHandler) UpdateUserStatus(c *fiber.Ctx) error { if err := h.checkAuth(c); err != nil { return err } loginID := c.Params("loginId") if decoded, err := url.QueryUnescape(loginID); err == nil { loginID = decoded } var req struct { Status string `json:"status"` // "enabled" or "disabled" } if err := c.BodyParser(&req); err != nil { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid body"}) } var user *descope.UserResponse var err error slog.Info("[Admin] Updating status", "loginID", loginID, "status", req.Status) if req.Status == "enabled" || req.Status == "active" { user, err = h.DescopeClient.Management.User().Activate(context.Background(), loginID) } else { user, err = h.DescopeClient.Management.User().Deactivate(context.Background(), loginID) } if err != nil { slog.Error("[Admin] Status update failed", "error", err) return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) } return c.JSON(fiber.Map{ "message": "Status updated", "user": user, }) } // UpdateUser - PATCH /api/v1/admin/users/:loginId func (h *AdminHandler) UpdateUser(c *fiber.Ctx) error { if err := h.checkAuth(c); err != nil { return err } loginID := c.Params("loginId") if decoded, err := url.QueryUnescape(loginID); err == nil { loginID = decoded } var req struct { Email *string `json:"email"` Phone *string `json:"phone"` DisplayName *string `json:"displayName"` } if err := c.BodyParser(&req); err != nil { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid body"}) } ctx := context.Background() var err error // Update Display Name if req.DisplayName != nil { _, err = h.DescopeClient.Management.User().UpdateDisplayName(ctx, loginID, *req.DisplayName) if err != nil { return c.Status(500).JSON(fiber.Map{"error": "Failed to update name: " + err.Error()}) } } // Update Email if req.Email != nil { _, err = h.DescopeClient.Management.User().UpdateEmail(ctx, loginID, *req.Email, true, false) // verified=true, addToLoginIDs=false if err != nil { return c.Status(500).JSON(fiber.Map{"error": "Failed to update email: " + err.Error()}) } } // Update Phone if req.Phone != nil { phone := *req.Phone // Normalize if strings.HasPrefix(phone, "010") { phone = "+82" + phone[1:] } _, err = h.DescopeClient.Management.User().UpdatePhone(ctx, loginID, phone, true, false) // verified=true, addToLoginIDs=false if err != nil { return c.Status(500).JSON(fiber.Map{"error": "Failed to update phone: " + err.Error()}) } } return c.JSON(fiber.Map{"message": "User updated successfully"}) } func (h *AdminHandler) CreateUser(c *fiber.Ctx) error { if err := h.checkAuth(c); err != nil { return err } if h.DescopeClient == nil { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Descope Client not configured"}) } var req CreateUserRequest if err := c.BodyParser(&req); err != nil { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid request body"}) } if req.LoginID == "" { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "LoginID is required"}) } // Normalize Phone normalizedPhone := req.Phone if normalizedPhone != "" { if strings.HasPrefix(normalizedPhone, "010") { normalizedPhone = "+82" + normalizedPhone[1:] } else if strings.HasPrefix(normalizedPhone, "82") { normalizedPhone = "+" + normalizedPhone } } userObj := &descope.UserRequest{ User: descope.User{ Email: req.Email, Phone: normalizedPhone, Name: req.DisplayName, }, VerifiedEmail: boolPtr(req.Email != ""), VerifiedPhone: boolPtr(normalizedPhone != ""), } // Add Roles if provided if len(req.Roles) > 0 { userObj.Roles = req.Roles } // Add Tenants if provided if len(req.Tenants) > 0 { // Convert map[string][]string to []*descope.AssociatedTenant userTenants := []*descope.AssociatedTenant{} for tenantID, roles := range req.Tenants { userTenants = append(userTenants, &descope.AssociatedTenant{ TenantID: tenantID, Roles: roles, }) } userObj.Tenants = userTenants } slog.Info("[Admin] Creating user", "loginID", req.LoginID, "email", req.Email, "phone", normalizedPhone) res, err := h.DescopeClient.Management.User().Create(context.Background(), req.LoginID, userObj) if err != nil { slog.Error("[Admin] Failed to create user", "error", err) return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) } return c.JSON(fiber.Map{ "message": "User created successfully", "user": res, }) }