1
0
forked from baron/baron-sso
Files
baron-sso/backend/internal/handler/admin_handler.go
2026-01-16 13:48:16 +09:00

281 lines
8.1 KiB
Go

package handler
import (
"context"
"log"
"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 {
log.Printf("Warning: Failed to initialize Descope Client for Admin: %v", err)
}
} else {
log.Println("Warning: 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 {
log.Printf("[Admin] ListUsers failed: %v", 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
}
log.Printf("[Admin] Deleting user: %s", loginID)
if err := h.DescopeClient.Management.User().Delete(context.Background(), loginID); err != nil {
log.Printf("[Admin] DeleteUser failed: %v", 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
log.Printf("[Admin] Updating status for %s to %s", loginID, 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 {
log.Printf("[Admin] Status update failed: %v", 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
}
log.Printf("[Admin] Creating user: %s (Email: %s, Phone: %s)", req.LoginID, req.Email, normalizedPhone)
res, err := h.DescopeClient.Management.User().Create(context.Background(), req.LoginID, userObj)
if err != nil {
log.Printf("[Admin] Failed to create user: %v", err)
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
}
return c.JSON(fiber.Map{
"message": "User created successfully",
"user": res,
})
}