diff --git a/backend/cmd/server/main.go b/backend/cmd/server/main.go
index 5fdd5e69..1984c339 100644
--- a/backend/cmd/server/main.go
+++ b/backend/cmd/server/main.go
@@ -290,7 +290,7 @@ func main() {
auditHandler := handler.NewAuditHandler(auditRepo)
authHandler := handler.NewAuthHandler(redisService, idpProvider, auditRepo, oathkeeperRepo, tenantService, ketoService, ketoOutboxRepo, userRepo, consentRepo, kratosAdminService)
authHandler.HeadlessJWKS = headlessJWKSCache
- adminHandler := handler.NewAdminHandler(ketoService, ketoOutboxRepo, developerService)
+ adminHandler := handler.NewAdminHandler(ketoService, ketoOutboxRepo)
devHandler := handler.NewDevHandler(redisService, secretRepo, consentRepo, relyingPartyService, ketoService, ketoOutboxRepo, tenantService, developerService, authHandler)
devHandler.HeadlessJWKS = headlessJWKSCache
devHandler.AuditRepo = auditRepo
diff --git a/backend/internal/handler/admin_handler.go b/backend/internal/handler/admin_handler.go
index b334746a..42ee1815 100644
--- a/backend/internal/handler/admin_handler.go
+++ b/backend/internal/handler/admin_handler.go
@@ -1,28 +1,23 @@
package handler
import (
- "baron-sso-backend/internal/domain"
"baron-sso-backend/internal/repository"
"baron-sso-backend/internal/service"
- "log/slog"
"runtime"
- "strconv"
"time"
"github.com/gofiber/fiber/v2"
)
type AdminHandler struct {
- Keto service.KetoService
- KetoOutbox repository.KetoOutboxRepository
- DeveloperSvc *service.DeveloperService
+ Keto service.KetoService
+ KetoOutbox repository.KetoOutboxRepository
}
-func NewAdminHandler(keto service.KetoService, ketoOutbox repository.KetoOutboxRepository, developerSvc *service.DeveloperService) *AdminHandler {
+func NewAdminHandler(keto service.KetoService, ketoOutbox repository.KetoOutboxRepository) *AdminHandler {
return &AdminHandler{
- Keto: keto,
- KetoOutbox: ketoOutbox,
- DeveloperSvc: developerSvc,
+ Keto: keto,
+ KetoOutbox: ketoOutbox,
}
}
@@ -49,80 +44,3 @@ func (h *AdminHandler) GetSystemStats(c *fiber.Ctx) error {
return c.Status(fiber.StatusOK).JSON(stats)
}
-
-func (h *AdminHandler) ListDeveloperRequests(c *fiber.Ctx) error {
- status := c.Query("status")
- requests, err := h.DeveloperSvc.ListRequests(c.Context(), status)
- if err != nil {
- return errorJSON(c, fiber.StatusInternalServerError, err.Error())
- }
- return c.JSON(requests)
-}
-
-func (h *AdminHandler) ApproveDeveloperRequest(c *fiber.Ctx) error {
- idStr := c.Params("id")
- id, err := strconv.ParseUint(idStr, 10, 32)
- if err != nil {
- return errorJSON(c, fiber.StatusBadRequest, "invalid request id")
- }
-
- var reqBody struct {
- AdminNotes string `json:"adminNotes"`
- }
- if err := c.BodyParser(&reqBody); err != nil {
- return errorJSON(c, fiber.StatusBadRequest, "invalid request body")
- }
-
- // 1. Get request to know userID and tenantID
- devReq, err := h.DeveloperSvc.GetRequestByID(c.Context(), uint(id))
- if err != nil {
- return errorJSON(c, fiber.StatusInternalServerError, "failed to fetch request details")
- }
-
- // 2. Approve in DB
- if err := h.DeveloperSvc.ApproveRequest(c.Context(), uint(id), reqBody.AdminNotes); err != nil {
- return errorJSON(c, fiber.StatusInternalServerError, err.Error())
- }
-
- // 3. Grant Keto Permissions via Outbox
- if h.KetoOutbox != nil {
- subject := "User:" + devReq.UserID
- permissions := []string{"view_dev_console", "grant_dev_permissions"}
-
- for _, relation := range permissions {
- err := h.KetoOutbox.Create(c.Context(), &domain.KetoOutbox{
- Namespace: "Tenant",
- Object: devReq.TenantID,
- Relation: relation,
- Subject: subject,
- Action: domain.KetoOutboxActionCreate,
- })
- if err != nil {
- slog.Warn("failed to create keto outbox for developer approval", "relation", relation, "userID", devReq.UserID, "error", err)
- }
- }
- }
-
- return c.JSON(fiber.Map{"status": "ok"})
-}
-
-func (h *AdminHandler) RejectDeveloperRequest(c *fiber.Ctx) error {
- idStr := c.Params("id")
- id, err := strconv.ParseUint(idStr, 10, 32)
- if err != nil {
- return errorJSON(c, fiber.StatusBadRequest, "invalid request id")
- }
-
- var reqBody struct {
- AdminNotes string `json:"adminNotes"`
- }
- if err := c.BodyParser(&reqBody); err != nil {
- return errorJSON(c, fiber.StatusBadRequest, "invalid request body")
- }
-
- if err := h.DeveloperSvc.RejectRequest(c.Context(), uint(id), reqBody.AdminNotes); err != nil {
- return errorJSON(c, fiber.StatusInternalServerError, err.Error())
- }
-
- return c.JSON(fiber.Map{"status": "ok"})
-}
diff --git a/backend/internal/handler/dev_handler.go b/backend/internal/handler/dev_handler.go
index 5282b667..4a3ce284 100644
--- a/backend/internal/handler/dev_handler.go
+++ b/backend/internal/handler/dev_handler.go
@@ -15,6 +15,7 @@ import (
"log/slog"
"net/http"
"os"
+ "strconv"
"strings"
"time"
@@ -2865,3 +2866,105 @@ func (h *DevHandler) GetDeveloperRequestStatus(c *fiber.Ctx) error {
return c.JSON(status)
}
+
+func (h *DevHandler) ListDeveloperRequests(c *fiber.Ctx) error {
+ profile := h.getCurrentProfile(c)
+ if profile == nil {
+ return errorJSON(c, fiber.StatusUnauthorized, "unauthorized")
+ }
+
+ role := normalizeUserRole(profile.Role)
+ status := c.Query("status")
+
+ userID := profile.ID
+ if role == domain.RoleSuperAdmin {
+ // Super Admin can see everyone's requests
+ userID = ""
+ }
+
+ requests, err := h.DeveloperSvc.ListRequests(c.Context(), userID, status)
+ if err != nil {
+ return errorJSON(c, fiber.StatusInternalServerError, err.Error())
+ }
+
+ return c.JSON(requests)
+}
+
+func (h *DevHandler) ApproveDeveloperRequest(c *fiber.Ctx) error {
+ profile := h.getCurrentProfile(c)
+ if profile == nil {
+ return errorJSON(c, fiber.StatusUnauthorized, "unauthorized")
+ }
+ if normalizeUserRole(profile.Role) != domain.RoleSuperAdmin {
+ return errorJSON(c, fiber.StatusForbidden, "forbidden: super_admin only")
+ }
+
+ idStr := c.Params("id")
+ id, err := strconv.ParseUint(idStr, 10, 32)
+ if err != nil {
+ return errorJSON(c, fiber.StatusBadRequest, "invalid request id")
+ }
+
+ var reqBody struct {
+ AdminNotes string `json:"adminNotes"`
+ }
+ if err := c.BodyParser(&reqBody); err != nil {
+ return errorJSON(c, fiber.StatusBadRequest, "invalid request body")
+ }
+
+ devReq, err := h.DeveloperSvc.GetRequestByID(c.Context(), uint(id))
+ if err != nil {
+ return errorJSON(c, fiber.StatusInternalServerError, "failed to fetch request details")
+ }
+
+ if err := h.DeveloperSvc.ApproveRequest(c.Context(), uint(id), reqBody.AdminNotes); err != nil {
+ return errorJSON(c, fiber.StatusInternalServerError, err.Error())
+ }
+
+ // Grant Keto Permissions
+ if h.KetoOutbox != nil {
+ subject := "User:" + devReq.UserID
+ permissions := []string{"view_dev_console", "grant_dev_permissions"}
+
+ for _, relation := range permissions {
+ _ = h.KetoOutbox.Create(c.Context(), &domain.KetoOutbox{
+ Namespace: "Tenant",
+ Object: devReq.TenantID,
+ Relation: relation,
+ Subject: subject,
+ Action: domain.KetoOutboxActionCreate,
+ })
+ }
+ }
+
+ return c.JSON(fiber.Map{"status": "ok"})
+}
+
+func (h *DevHandler) RejectDeveloperRequest(c *fiber.Ctx) error {
+ profile := h.getCurrentProfile(c)
+ if profile == nil {
+ return errorJSON(c, fiber.StatusUnauthorized, "unauthorized")
+ }
+ if normalizeUserRole(profile.Role) != domain.RoleSuperAdmin {
+ return errorJSON(c, fiber.StatusForbidden, "forbidden: super_admin only")
+ }
+
+ idStr := c.Params("id")
+ id, err := strconv.ParseUint(idStr, 10, 32)
+ if err != nil {
+ return errorJSON(c, fiber.StatusBadRequest, "invalid request id")
+ }
+
+ var reqBody struct {
+ AdminNotes string `json:"adminNotes"`
+ }
+ if err := c.BodyParser(&reqBody); err != nil {
+ return errorJSON(c, fiber.StatusBadRequest, "invalid request body")
+ }
+
+ if err := h.DeveloperSvc.RejectRequest(c.Context(), uint(id), reqBody.AdminNotes); err != nil {
+ return errorJSON(c, fiber.StatusInternalServerError, err.Error())
+ }
+
+ return c.JSON(fiber.Map{"status": "ok"})
+}
diff --git a/backend/internal/service/developer_service.go b/backend/internal/service/developer_service.go
index 8097b023..6040e83a 100644
--- a/backend/internal/service/developer_service.go
+++ b/backend/internal/service/developer_service.go
@@ -48,9 +48,12 @@ func (s *DeveloperService) GetRequestByID(ctx context.Context, id uint) (*domain
return &req, nil
}
-func (s *DeveloperService) ListRequests(ctx context.Context, status string) ([]domain.DeveloperRequest, error) {
+func (s *DeveloperService) ListRequests(ctx context.Context, userID, status string) ([]domain.DeveloperRequest, error) {
var requests []domain.DeveloperRequest
query := s.db.WithContext(ctx)
+ if userID != "" {
+ query = query.Where("user_id = ?", userID)
+ }
if status != "" {
query = query.Where("status = ?", status)
}
diff --git a/devfront/src/app/routes.tsx b/devfront/src/app/routes.tsx
index 18272285..03cd6148 100644
--- a/devfront/src/app/routes.tsx
+++ b/devfront/src/app/routes.tsx
@@ -9,6 +9,7 @@ import ClientDetailsPage from "../features/clients/ClientDetailsPage";
import ClientGeneralPage from "../features/clients/ClientGeneralPage";
import ClientRelationsPage from "../features/clients/ClientRelationsPage";
import ClientsPage from "../features/clients/ClientsPage";
+import DeveloperRequestPage from "../features/developer-request/DeveloperRequestPage";
import ProfilePage from "../features/profile/ProfilePage";
export const router = createBrowserRouter(
@@ -38,6 +39,7 @@ export const router = createBrowserRouter(
path: "clients/:id/relationships",
element:
-
+ {isSuperAdmin + ? t( + "msg.dev.request.admin_desc", + "사용자들의 개발자 권한 신청 내역을 관리합니다.", + ) + : t( + "msg.dev.request.user_desc", + "내 신청 내역을 확인하고 새로운 권한을 신청할 수 있습니다.", + )} +
++ {t( + "msg.dev.request.modal.desc", + "신청 사유를 입력해 주세요. 관리자 확인 후 승인됩니다.", + )} +
+