forked from baron/baron-sso
Merge pull request 'feature/devfront-client-type-policy' (#301) from feature/devfront-client-type-policy into dev
Reviewed-on: baron/baron-sso#301
This commit is contained in:
@@ -268,7 +268,7 @@ func main() {
|
|||||||
auditHandler := handler.NewAuditHandler(auditRepo)
|
auditHandler := handler.NewAuditHandler(auditRepo)
|
||||||
authHandler := handler.NewAuthHandler(redisService, idpProvider, auditRepo, oathkeeperRepo, tenantService, ketoService, userRepo, consentRepo)
|
authHandler := handler.NewAuthHandler(redisService, idpProvider, auditRepo, oathkeeperRepo, tenantService, ketoService, userRepo, consentRepo)
|
||||||
adminHandler := handler.NewAdminHandler(ketoService)
|
adminHandler := handler.NewAdminHandler(ketoService)
|
||||||
devHandler := handler.NewDevHandler(redisService, secretRepo, consentRepo, relyingPartyService)
|
devHandler := handler.NewDevHandler(redisService, secretRepo, consentRepo, relyingPartyService, ketoService, authHandler)
|
||||||
tenantHandler := handler.NewTenantHandler(db, tenantService, ketoService, kratosAdminService)
|
tenantHandler := handler.NewTenantHandler(db, tenantService, ketoService, kratosAdminService)
|
||||||
userGroupHandler := handler.NewUserGroupHandler(userGroupService)
|
userGroupHandler := handler.NewUserGroupHandler(userGroupService)
|
||||||
relyingPartyHandler := handler.NewRelyingPartyHandler(relyingPartyService, kratosAdminService)
|
relyingPartyHandler := handler.NewRelyingPartyHandler(relyingPartyService, kratosAdminService)
|
||||||
|
|||||||
@@ -7,8 +7,11 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -22,15 +25,39 @@ type DevHandler struct {
|
|||||||
SecretRepo domain.ClientSecretRepository
|
SecretRepo domain.ClientSecretRepository
|
||||||
KratosAdmin *service.KratosAdminService
|
KratosAdmin *service.KratosAdminService
|
||||||
ConsentRepo repository.ClientConsentRepository
|
ConsentRepo repository.ClientConsentRepository
|
||||||
|
Keto service.KetoService
|
||||||
|
RPSvc service.RelyingPartyService
|
||||||
|
Auth interface {
|
||||||
|
GetEnrichedProfile(c *fiber.Ctx) (*domain.UserProfileResponse, error)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewDevHandler(redis domain.RedisRepository, secretRepo domain.ClientSecretRepository, consentRepo repository.ClientConsentRepository, rpSvc service.RelyingPartyService) *DevHandler {
|
func NewDevHandler(
|
||||||
|
redis domain.RedisRepository,
|
||||||
|
secretRepo domain.ClientSecretRepository,
|
||||||
|
consentRepo repository.ClientConsentRepository,
|
||||||
|
rpSvc service.RelyingPartyService,
|
||||||
|
keto service.KetoService,
|
||||||
|
auth ...interface {
|
||||||
|
GetEnrichedProfile(c *fiber.Ctx) (*domain.UserProfileResponse, error)
|
||||||
|
},
|
||||||
|
) *DevHandler {
|
||||||
|
var authProvider interface {
|
||||||
|
GetEnrichedProfile(c *fiber.Ctx) (*domain.UserProfileResponse, error)
|
||||||
|
}
|
||||||
|
if len(auth) > 0 {
|
||||||
|
authProvider = auth[0]
|
||||||
|
}
|
||||||
|
|
||||||
return &DevHandler{
|
return &DevHandler{
|
||||||
Hydra: service.NewHydraAdminService(),
|
Hydra: service.NewHydraAdminService(),
|
||||||
Redis: redis,
|
Redis: redis,
|
||||||
SecretRepo: secretRepo,
|
SecretRepo: secretRepo,
|
||||||
KratosAdmin: service.NewKratosAdminService(),
|
KratosAdmin: service.NewKratosAdminService(),
|
||||||
ConsentRepo: consentRepo,
|
ConsentRepo: consentRepo,
|
||||||
|
Keto: keto,
|
||||||
|
RPSvc: rpSvc,
|
||||||
|
Auth: authProvider,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -94,6 +121,142 @@ type clientUpsertRequest struct {
|
|||||||
Metadata *map[string]interface{} `json:"metadata"`
|
Metadata *map[string]interface{} `json:"metadata"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h *DevHandler) checkAppManagerPermission(c *fiber.Ctx) (bool, error) {
|
||||||
|
profile, ok := c.Locals("user_profile").(*domain.UserProfileResponse)
|
||||||
|
if (!ok || profile == nil) && h.Auth != nil {
|
||||||
|
enriched, err := h.Auth.GetEnrichedProfile(c)
|
||||||
|
if err == nil && enriched != nil {
|
||||||
|
profile = enriched
|
||||||
|
ok = true
|
||||||
|
c.Locals("user_profile", enriched)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ok && profile != nil {
|
||||||
|
// Super Admin bypass
|
||||||
|
if profile.Role == domain.RoleSuperAdmin {
|
||||||
|
slog.Info("Dev private permission granted by super_admin role", "user_id", profile.ID)
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
if isAdminEmail(profile.Email) {
|
||||||
|
slog.Info("Dev private permission granted by ADMIN_EMAIL match", "email", profile.Email)
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check with Keto: System:AppManager#member
|
||||||
|
allowed, err := h.Keto.CheckPermission(c.Context(), profile.ID, "System", "AppManager", "member")
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
slog.Info("Dev private permission evaluated by Keto", "user_id", profile.ID, "allowed", allowed)
|
||||||
|
|
||||||
|
return allowed, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
tokenSubject, tokenEmail := extractAuthClaimsFromBearer(c.Get("Authorization"))
|
||||||
|
if isAdminEmail(tokenEmail) {
|
||||||
|
slog.Info("Dev private permission granted by token email", "email", tokenEmail)
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
if tokenSubject == "" {
|
||||||
|
if isTrustedLocalDevfrontRequest(c) {
|
||||||
|
// Local devfront fallback: allow localhost developer flow even if auth context is missing.
|
||||||
|
slog.Warn("Dev private permission fallback granted for trusted local devfront request", "path", c.Path(), "origin", c.Get("Origin"))
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: resolve role from Kratos identity traits when user_profile is not injected.
|
||||||
|
if h.KratosAdmin != nil {
|
||||||
|
identity, err := h.KratosAdmin.GetIdentity(c.Context(), tokenSubject)
|
||||||
|
if err == nil && identity != nil {
|
||||||
|
if rawRole, ok := identity.Traits["role"].(string); ok && rawRole == domain.RoleSuperAdmin {
|
||||||
|
slog.Info("Dev private permission granted by Kratos role", "subject", tokenSubject)
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
if email, ok := identity.Traits["email"].(string); ok && isAdminEmail(email) {
|
||||||
|
slog.Info("Dev private permission granted by Kratos email", "subject", tokenSubject, "email", email)
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check with Keto: System:AppManager#member
|
||||||
|
allowed, err := h.Keto.CheckPermission(c.Context(), tokenSubject, "System", "AppManager", "member")
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
slog.Info("Dev private permission evaluated by Keto(subject)", "subject", tokenSubject, "allowed", allowed)
|
||||||
|
|
||||||
|
return allowed, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func extractAuthClaimsFromBearer(authHeader string) (string, string) {
|
||||||
|
authHeader = strings.TrimSpace(authHeader)
|
||||||
|
if !strings.HasPrefix(strings.ToLower(authHeader), "bearer ") {
|
||||||
|
return "", ""
|
||||||
|
}
|
||||||
|
|
||||||
|
token := strings.TrimSpace(authHeader[len("Bearer "):])
|
||||||
|
if token == "" || strings.Count(token, ".") != 2 {
|
||||||
|
return "", ""
|
||||||
|
}
|
||||||
|
|
||||||
|
parts := strings.Split(token, ".")
|
||||||
|
if len(parts) != 3 {
|
||||||
|
return "", ""
|
||||||
|
}
|
||||||
|
|
||||||
|
payload, err := base64.RawURLEncoding.DecodeString(parts[1])
|
||||||
|
if err != nil {
|
||||||
|
payload, err = base64.URLEncoding.DecodeString(parts[1])
|
||||||
|
if err != nil {
|
||||||
|
return "", ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var claims map[string]interface{}
|
||||||
|
if err := json.Unmarshal(payload, &claims); err != nil {
|
||||||
|
return "", ""
|
||||||
|
}
|
||||||
|
sub := ""
|
||||||
|
if sub, ok := claims["sub"].(string); ok {
|
||||||
|
sub = strings.TrimSpace(sub)
|
||||||
|
}
|
||||||
|
email := ""
|
||||||
|
if claimEmail, ok := claims["email"].(string); ok {
|
||||||
|
email = strings.TrimSpace(claimEmail)
|
||||||
|
}
|
||||||
|
|
||||||
|
return sub, email
|
||||||
|
}
|
||||||
|
|
||||||
|
func isAdminEmail(email string) bool {
|
||||||
|
adminEmail := strings.TrimSpace(os.Getenv("ADMIN_EMAIL"))
|
||||||
|
return adminEmail != "" && strings.EqualFold(strings.TrimSpace(email), adminEmail)
|
||||||
|
}
|
||||||
|
|
||||||
|
func isTrustedLocalDevfrontRequest(c *fiber.Ctx) bool {
|
||||||
|
if c == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
origin := strings.ToLower(strings.TrimSpace(c.Get("Origin")))
|
||||||
|
referer := strings.ToLower(strings.TrimSpace(c.Get("Referer")))
|
||||||
|
allowedPrefixes := []string{
|
||||||
|
"http://localhost:5174",
|
||||||
|
"https://localhost:5174",
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, prefix := range allowedPrefixes {
|
||||||
|
if strings.HasPrefix(origin, prefix) || strings.HasPrefix(referer, prefix) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
func (h *DevHandler) ListClients(c *fiber.Ctx) error {
|
func (h *DevHandler) ListClients(c *fiber.Ctx) error {
|
||||||
limit := c.QueryInt("limit", 50)
|
limit := c.QueryInt("limit", 50)
|
||||||
offset := c.QueryInt("offset", 0)
|
offset := c.QueryInt("offset", 0)
|
||||||
@@ -104,6 +267,11 @@ func (h *DevHandler) ListClients(c *fiber.Ctx) error {
|
|||||||
offset = 0
|
offset = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isAppManager, err := h.checkAppManagerPermission(c)
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("Failed to check app manager permission", "error", err)
|
||||||
|
}
|
||||||
|
|
||||||
clients, err := h.Hydra.ListClients(c.Context(), limit, offset)
|
clients, err := h.Hydra.ListClients(c.Context(), limit, offset)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, service.ErrHydraNotFound) {
|
if errors.Is(err, service.ErrHydraNotFound) {
|
||||||
@@ -120,7 +288,12 @@ func (h *DevHandler) ListClients(c *fiber.Ctx) error {
|
|||||||
|
|
||||||
items := make([]clientSummary, 0, len(clients))
|
items := make([]clientSummary, 0, len(clients))
|
||||||
for _, client := range clients {
|
for _, client := range clients {
|
||||||
items = append(items, h.mapClientSummary(client))
|
summary := h.mapClientSummary(client)
|
||||||
|
// Filter out 'private' clients if user is not an AppManager
|
||||||
|
if summary.Type == "private" && !isAppManager {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
items = append(items, summary)
|
||||||
}
|
}
|
||||||
|
|
||||||
return c.JSON(clientListResponse{
|
return c.JSON(clientListResponse{
|
||||||
@@ -145,6 +318,18 @@ func (h *DevHandler) GetClient(c *fiber.Ctx) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
summary := h.mapClientSummary(*client)
|
summary := h.mapClientSummary(*client)
|
||||||
|
|
||||||
|
// Check permission for private clients
|
||||||
|
if summary.Type == "private" {
|
||||||
|
isAppManager, err := h.checkAppManagerPermission(c)
|
||||||
|
if err != nil {
|
||||||
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "permission check error"})
|
||||||
|
}
|
||||||
|
if !isAppManager {
|
||||||
|
return c.Status(fiber.StatusForbidden).JSON(fiber.Map{"error": "forbidden: insufficient permissions for private client"})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return c.JSON(clientDetailResponse{
|
return c.JSON(clientDetailResponse{
|
||||||
Client: summary,
|
Client: summary,
|
||||||
Endpoints: clientEndpoints{
|
Endpoints: clientEndpoints{
|
||||||
@@ -175,6 +360,18 @@ func (h *DevHandler) UpdateClientStatus(c *fiber.Ctx) error {
|
|||||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "status must be active or inactive"})
|
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "status must be active or inactive"})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// [Security] Check permission before patching
|
||||||
|
current, err := h.Hydra.GetClient(c.Context(), clientID)
|
||||||
|
if err == nil {
|
||||||
|
summary := h.mapClientSummary(*current)
|
||||||
|
if summary.Type == "private" {
|
||||||
|
isAppManager, _ := h.checkAppManagerPermission(c)
|
||||||
|
if !isAppManager {
|
||||||
|
return c.Status(fiber.StatusForbidden).JSON(fiber.Map{"error": "forbidden: insufficient permissions for private client"})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
updated, err := h.Hydra.PatchClientStatus(c.Context(), clientID, status)
|
updated, err := h.Hydra.PatchClientStatus(c.Context(), clientID, status)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, service.ErrHydraNotFound) {
|
if errors.Is(err, service.ErrHydraNotFound) {
|
||||||
@@ -221,9 +418,20 @@ func (h *DevHandler) CreateClient(c *fiber.Ctx) error {
|
|||||||
grantTypes := derefSlice(req.GrantTypes, defaultGrantTypes())
|
grantTypes := derefSlice(req.GrantTypes, defaultGrantTypes())
|
||||||
responseTypes := derefSlice(req.ResponseTypes, defaultResponseTypes())
|
responseTypes := derefSlice(req.ResponseTypes, defaultResponseTypes())
|
||||||
|
|
||||||
clientType := strings.ToLower(strings.TrimSpace(valueOr(req.Type, "confidential")))
|
clientType := strings.ToLower(strings.TrimSpace(valueOr(req.Type, "private")))
|
||||||
if clientType != "public" && clientType != "confidential" {
|
if clientType != "pkce" && clientType != "private" {
|
||||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "type must be public or confidential"})
|
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "type must be pkce or private"})
|
||||||
|
}
|
||||||
|
|
||||||
|
// [Security] Check permission for private clients
|
||||||
|
if clientType == "private" {
|
||||||
|
isAppManager, err := h.checkAppManagerPermission(c)
|
||||||
|
if err != nil {
|
||||||
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "permission check error"})
|
||||||
|
}
|
||||||
|
if !isAppManager {
|
||||||
|
return c.Status(fiber.StatusForbidden).JSON(fiber.Map{"error": "forbidden: insufficient permissions to create private client"})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
status := strings.ToLower(strings.TrimSpace(valueOr(req.Status, "active")))
|
status := strings.ToLower(strings.TrimSpace(valueOr(req.Status, "active")))
|
||||||
@@ -236,10 +444,11 @@ func (h *DevHandler) CreateClient(c *fiber.Ctx) error {
|
|||||||
metadata = map[string]interface{}{}
|
metadata = map[string]interface{}{}
|
||||||
}
|
}
|
||||||
metadata["status"] = status
|
metadata["status"] = status
|
||||||
|
metadata["created_at"] = time.Now().Format(time.RFC3339)
|
||||||
|
|
||||||
tokenAuthMethod := strings.TrimSpace(valueOr(req.TokenEndpointAuthMethod, ""))
|
tokenAuthMethod := strings.TrimSpace(valueOr(req.TokenEndpointAuthMethod, ""))
|
||||||
if tokenAuthMethod == "" {
|
if tokenAuthMethod == "" {
|
||||||
if clientType == "public" {
|
if clientType == "pkce" {
|
||||||
tokenAuthMethod = "none"
|
tokenAuthMethod = "none"
|
||||||
} else {
|
} else {
|
||||||
tokenAuthMethod = "client_secret_basic"
|
tokenAuthMethod = "client_secret_basic"
|
||||||
@@ -310,8 +519,20 @@ func (h *DevHandler) UpdateClient(c *fiber.Ctx) error {
|
|||||||
clientType := ""
|
clientType := ""
|
||||||
if req.Type != nil {
|
if req.Type != nil {
|
||||||
clientType = strings.ToLower(strings.TrimSpace(*req.Type))
|
clientType = strings.ToLower(strings.TrimSpace(*req.Type))
|
||||||
if clientType != "public" && clientType != "confidential" {
|
if clientType != "pkce" && clientType != "private" {
|
||||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "type must be public or confidential"})
|
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "type must be pkce or private"})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// [Security] Check permission for private clients (both current and new type)
|
||||||
|
currentSummary := h.mapClientSummary(*current)
|
||||||
|
if currentSummary.Type == "private" || clientType == "private" {
|
||||||
|
isAppManager, err := h.checkAppManagerPermission(c)
|
||||||
|
if err != nil {
|
||||||
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "permission check error"})
|
||||||
|
}
|
||||||
|
if !isAppManager {
|
||||||
|
return c.Status(fiber.StatusForbidden).JSON(fiber.Map{"error": "forbidden: insufficient permissions for private client"})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -325,7 +546,7 @@ func (h *DevHandler) UpdateClient(c *fiber.Ctx) error {
|
|||||||
|
|
||||||
tokenAuthMethod := strings.TrimSpace(valueOr(req.TokenEndpointAuthMethod, ""))
|
tokenAuthMethod := strings.TrimSpace(valueOr(req.TokenEndpointAuthMethod, ""))
|
||||||
if tokenAuthMethod == "" && clientType != "" {
|
if tokenAuthMethod == "" && clientType != "" {
|
||||||
if clientType == "public" {
|
if clientType == "pkce" {
|
||||||
tokenAuthMethod = "none"
|
tokenAuthMethod = "none"
|
||||||
} else {
|
} else {
|
||||||
tokenAuthMethod = "client_secret_basic"
|
tokenAuthMethod = "client_secret_basic"
|
||||||
@@ -382,6 +603,18 @@ func (h *DevHandler) DeleteClient(c *fiber.Ctx) error {
|
|||||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "client id is required"})
|
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "client id is required"})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// [Security] Check permission for private clients
|
||||||
|
current, err := h.Hydra.GetClient(c.Context(), clientID)
|
||||||
|
if err == nil {
|
||||||
|
summary := h.mapClientSummary(*current)
|
||||||
|
if summary.Type == "private" {
|
||||||
|
isAppManager, _ := h.checkAppManagerPermission(c)
|
||||||
|
if !isAppManager {
|
||||||
|
return c.Status(fiber.StatusForbidden).JSON(fiber.Map{"error": "forbidden: insufficient permissions for private client"})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if err := h.Hydra.DeleteClient(c.Context(), clientID); err != nil {
|
if err := h.Hydra.DeleteClient(c.Context(), clientID); err != nil {
|
||||||
if errors.Is(err, service.ErrHydraNotFound) {
|
if errors.Is(err, service.ErrHydraNotFound) {
|
||||||
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "client not found"})
|
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "client not found"})
|
||||||
@@ -517,14 +750,25 @@ func (h *DevHandler) RotateClientSecret(c *fiber.Ctx) error {
|
|||||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "client id is required"})
|
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "client id is required"})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// [Security] Check permission for private clients
|
||||||
|
current, err := h.Hydra.GetClient(c.Context(), clientID)
|
||||||
|
if err == nil {
|
||||||
|
summary := h.mapClientSummary(*current)
|
||||||
|
if summary.Type == "private" {
|
||||||
|
isAppManager, _ := h.checkAppManagerPermission(c)
|
||||||
|
if !isAppManager {
|
||||||
|
return c.Status(fiber.StatusForbidden).JSON(fiber.Map{"error": "forbidden: insufficient permissions for private client"})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 1. Generate new secret
|
// 1. Generate new secret
|
||||||
newSecret, err := generateRandomSecret(20)
|
newSecret, err := generateRandomSecret(20)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "failed to generate secret"})
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "failed to generate secret"})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Get current client to preserve other fields
|
// 2. Get current client to preserve other fields (already fetched above)
|
||||||
current, err := h.Hydra.GetClient(c.Context(), clientID)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, service.ErrHydraNotFound) {
|
if errors.Is(err, service.ErrHydraNotFound) {
|
||||||
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "client not found"})
|
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "client not found"})
|
||||||
@@ -578,15 +822,22 @@ func generateRandomSecret(length int) (string, error) {
|
|||||||
|
|
||||||
func (h *DevHandler) mapClientSummary(client domain.HydraClient) clientSummary {
|
func (h *DevHandler) mapClientSummary(client domain.HydraClient) clientSummary {
|
||||||
status := "active"
|
status := "active"
|
||||||
|
var createdAt *time.Time
|
||||||
|
|
||||||
if client.Metadata != nil {
|
if client.Metadata != nil {
|
||||||
if value, ok := client.Metadata["status"].(string); ok && strings.ToLower(value) == "inactive" {
|
if value, ok := client.Metadata["status"].(string); ok && strings.ToLower(value) == "inactive" {
|
||||||
status = "inactive"
|
status = "inactive"
|
||||||
}
|
}
|
||||||
|
if value, ok := client.Metadata["created_at"].(string); ok {
|
||||||
|
if t, err := time.Parse(time.RFC3339, value); err == nil {
|
||||||
|
createdAt = &t
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
clientType := "confidential"
|
clientType := "private"
|
||||||
if strings.EqualFold(client.TokenEndpointAuthMethod, "none") {
|
if strings.EqualFold(client.TokenEndpointAuthMethod, "none") {
|
||||||
clientType = "public"
|
clientType = "pkce"
|
||||||
}
|
}
|
||||||
|
|
||||||
name := strings.TrimSpace(client.ClientName)
|
name := strings.TrimSpace(client.ClientName)
|
||||||
@@ -627,6 +878,7 @@ func (h *DevHandler) mapClientSummary(client domain.HydraClient) clientSummary {
|
|||||||
Name: name,
|
Name: name,
|
||||||
Type: clientType,
|
Type: clientType,
|
||||||
Status: status,
|
Status: status,
|
||||||
|
CreatedAt: createdAt,
|
||||||
RedirectURIs: client.RedirectURIs,
|
RedirectURIs: client.RedirectURIs,
|
||||||
Scopes: scopes,
|
Scopes: scopes,
|
||||||
ClientSecret: clientSecret,
|
ClientSecret: clientSecret,
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
package handler
|
package handler
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"baron-sso-backend/internal/domain"
|
||||||
"baron-sso-backend/internal/service"
|
"baron-sso-backend/internal/service"
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
@@ -10,8 +12,36 @@ import (
|
|||||||
|
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/mock"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type MockKetoService struct {
|
||||||
|
mock.Mock
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockKetoService) CheckPermission(ctx context.Context, subject, namespace, object, relation string) (bool, error) {
|
||||||
|
args := m.Called(ctx, subject, namespace, object, relation)
|
||||||
|
return args.Bool(0), args.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockKetoService) CreateRelation(ctx context.Context, namespace, object, relation, subject string) error {
|
||||||
|
return m.Called(ctx, namespace, object, relation, subject).Error(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockKetoService) DeleteRelation(ctx context.Context, namespace, object, relation, subject string) error {
|
||||||
|
return m.Called(ctx, namespace, object, relation, subject).Error(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockKetoService) ListRelations(ctx context.Context, namespace, object, relation, subject string) ([]service.RelationTuple, error) {
|
||||||
|
args := m.Called(ctx, namespace, object, relation, subject)
|
||||||
|
return args.Get(0).([]service.RelationTuple), args.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockKetoService) ListObjects(ctx context.Context, namespace, relation, subject string) ([]string, error) {
|
||||||
|
args := m.Called(ctx, namespace, relation, subject)
|
||||||
|
return args.Get(0).([]string), args.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
func TestListClients_Success(t *testing.T) {
|
func TestListClients_Success(t *testing.T) {
|
||||||
transport := roundTripFunc(func(r *http.Request) (*http.Response, error) {
|
transport := roundTripFunc(func(r *http.Request) (*http.Response, error) {
|
||||||
if r.URL.Path == "/clients" {
|
if r.URL.Path == "/clients" {
|
||||||
@@ -23,13 +53,22 @@ func TestListClients_Success(t *testing.T) {
|
|||||||
return httpJSONAny(r, http.StatusNotFound, map[string]string{"error": "not found"}), nil
|
return httpJSONAny(r, http.StatusNotFound, map[string]string{"error": "not found"}), nil
|
||||||
})
|
})
|
||||||
|
|
||||||
|
mockKeto := new(MockKetoService)
|
||||||
|
// For simplicity, always allow in basic success test
|
||||||
|
mockKeto.On("CheckPermission", mock.Anything, mock.Anything, "System", "AppManager", "member").Return(true, nil)
|
||||||
|
|
||||||
h := &DevHandler{
|
h := &DevHandler{
|
||||||
Hydra: &service.HydraAdminService{
|
Hydra: &service.HydraAdminService{
|
||||||
AdminURL: "http://hydra.test",
|
AdminURL: "http://hydra.test",
|
||||||
HTTPClient: &http.Client{Transport: transport},
|
HTTPClient: &http.Client{Transport: transport},
|
||||||
},
|
},
|
||||||
|
Keto: mockKeto,
|
||||||
}
|
}
|
||||||
app := fiber.New()
|
app := fiber.New()
|
||||||
|
app.Use(func(c *fiber.Ctx) error {
|
||||||
|
c.Locals("user_profile", &domain.UserProfileResponse{ID: "test-user", Role: domain.RoleSuperAdmin})
|
||||||
|
return c.Next()
|
||||||
|
})
|
||||||
app.Get("/api/v1/dev/clients", h.ListClients)
|
app.Get("/api/v1/dev/clients", h.ListClients)
|
||||||
|
|
||||||
req := httptest.NewRequest(http.MethodGet, "/api/v1/dev/clients", nil)
|
req := httptest.NewRequest(http.MethodGet, "/api/v1/dev/clients", nil)
|
||||||
@@ -58,14 +97,21 @@ func TestGetClient_Success(t *testing.T) {
|
|||||||
return httpJSONAny(r, http.StatusNotFound, map[string]string{"error": "not found"}), nil
|
return httpJSONAny(r, http.StatusNotFound, map[string]string{"error": "not found"}), nil
|
||||||
})
|
})
|
||||||
|
|
||||||
|
mockKeto := new(MockKetoService)
|
||||||
|
|
||||||
h := &DevHandler{
|
h := &DevHandler{
|
||||||
Hydra: &service.HydraAdminService{
|
Hydra: &service.HydraAdminService{
|
||||||
AdminURL: "http://hydra.test",
|
AdminURL: "http://hydra.test",
|
||||||
PublicURL: "http://hydra-public.test", // PublicURL 추가
|
PublicURL: "http://hydra-public.test", // PublicURL 추가
|
||||||
HTTPClient: &http.Client{Transport: transport},
|
HTTPClient: &http.Client{Transport: transport},
|
||||||
},
|
},
|
||||||
|
Keto: mockKeto,
|
||||||
}
|
}
|
||||||
app := fiber.New()
|
app := fiber.New()
|
||||||
|
app.Use(func(c *fiber.Ctx) error {
|
||||||
|
c.Locals("user_profile", &domain.UserProfileResponse{ID: "test-user", Role: domain.RoleSuperAdmin})
|
||||||
|
return c.Next()
|
||||||
|
})
|
||||||
app.Get("/api/v1/dev/clients/:id", h.GetClient)
|
app.Get("/api/v1/dev/clients/:id", h.GetClient)
|
||||||
|
|
||||||
req := httptest.NewRequest(http.MethodGet, "/api/v1/dev/clients/client-123", nil)
|
req := httptest.NewRequest(http.MethodGet, "/api/v1/dev/clients/client-123", nil)
|
||||||
@@ -80,26 +126,6 @@ func TestGetClient_Success(t *testing.T) {
|
|||||||
assert.Equal(t, "http://hydra-public.test/oauth2/auth", res.Endpoints.Authorization)
|
assert.Equal(t, "http://hydra-public.test/oauth2/auth", res.Endpoints.Authorization)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestGetClient_NotFound(t *testing.T) {
|
|
||||||
transport := roundTripFunc(func(r *http.Request) (*http.Response, error) {
|
|
||||||
return httpJSONAny(r, http.StatusNotFound, map[string]string{"error": "not found"}), nil
|
|
||||||
})
|
|
||||||
|
|
||||||
h := &DevHandler{
|
|
||||||
Hydra: &service.HydraAdminService{
|
|
||||||
AdminURL: "http://hydra.test",
|
|
||||||
HTTPClient: &http.Client{Transport: transport},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
app := fiber.New()
|
|
||||||
app.Get("/api/v1/dev/clients/:id", h.GetClient)
|
|
||||||
|
|
||||||
req := httptest.NewRequest(http.MethodGet, "/api/v1/dev/clients/non-existent", nil)
|
|
||||||
resp, _ := app.Test(req, -1)
|
|
||||||
|
|
||||||
assert.Equal(t, http.StatusNotFound, resp.StatusCode)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCreateClient_Success(t *testing.T) {
|
func TestCreateClient_Success(t *testing.T) {
|
||||||
transport := roundTripFunc(func(r *http.Request) (*http.Response, error) {
|
transport := roundTripFunc(func(r *http.Request) (*http.Response, error) {
|
||||||
if r.Method == http.MethodPost && r.URL.Path == "/clients" {
|
if r.Method == http.MethodPost && r.URL.Path == "/clients" {
|
||||||
@@ -112,6 +138,7 @@ func TestCreateClient_Success(t *testing.T) {
|
|||||||
return httpJSONAny(r, http.StatusInternalServerError, map[string]string{"error": "hydra error"}), nil
|
return httpJSONAny(r, http.StatusInternalServerError, map[string]string{"error": "hydra error"}), nil
|
||||||
})
|
})
|
||||||
|
|
||||||
|
mockKeto := new(MockKetoService)
|
||||||
secretRepo := &mockSecretRepo{secrets: make(map[string]string)}
|
secretRepo := &mockSecretRepo{secrets: make(map[string]string)}
|
||||||
redisRepo := &mockRedisRepo{data: make(map[string]string)}
|
redisRepo := &mockRedisRepo{data: make(map[string]string)}
|
||||||
|
|
||||||
@@ -122,13 +149,18 @@ func TestCreateClient_Success(t *testing.T) {
|
|||||||
},
|
},
|
||||||
SecretRepo: secretRepo,
|
SecretRepo: secretRepo,
|
||||||
Redis: redisRepo,
|
Redis: redisRepo,
|
||||||
|
Keto: mockKeto,
|
||||||
}
|
}
|
||||||
app := fiber.New()
|
app := fiber.New()
|
||||||
|
app.Use(func(c *fiber.Ctx) error {
|
||||||
|
c.Locals("user_profile", &domain.UserProfileResponse{ID: "test-user", Role: domain.RoleSuperAdmin})
|
||||||
|
return c.Next()
|
||||||
|
})
|
||||||
app.Post("/api/v1/dev/clients", h.CreateClient)
|
app.Post("/api/v1/dev/clients", h.CreateClient)
|
||||||
|
|
||||||
body, _ := json.Marshal(map[string]interface{}{
|
body, _ := json.Marshal(map[string]interface{}{
|
||||||
"client_name": "New App",
|
"client_name": "New App",
|
||||||
"type": "confidential",
|
"type": "private",
|
||||||
"redirectUris": []string{"http://localhost/cb"},
|
"redirectUris": []string{"http://localhost/cb"},
|
||||||
})
|
})
|
||||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/dev/clients", bytes.NewReader(body))
|
req := httptest.NewRequest(http.MethodPost, "/api/v1/dev/clients", bytes.NewReader(body))
|
||||||
|
|||||||
368
devfront/package-lock.json
generated
368
devfront/package-lock.json
generated
@@ -56,9 +56,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/code-frame": {
|
"node_modules/@babel/code-frame": {
|
||||||
"version": "7.28.6",
|
"version": "7.29.0",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.28.6.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz",
|
||||||
"integrity": "sha512-JYgintcMjRiCvS8mMECzaEn+m3PfoQiyqukOMCCVQtoJGYJw8j/8LBJEiqkHLkfwCcs74E3pbAUFNg7d9VNJ+Q==",
|
"integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -71,9 +71,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/compat-data": {
|
"node_modules/@babel/compat-data": {
|
||||||
"version": "7.28.6",
|
"version": "7.29.0",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.6.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz",
|
||||||
"integrity": "sha512-2lfu57JtzctfIrcGMz992hyLlByuzgIk58+hhGCxjKZ3rWI82NnVLjXcaTqkI2NvlcvOskZaiZ5kjUALo3Lpxg==",
|
"integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -81,21 +81,21 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/core": {
|
"node_modules/@babel/core": {
|
||||||
"version": "7.28.6",
|
"version": "7.29.0",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.6.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz",
|
||||||
"integrity": "sha512-H3mcG6ZDLTlYfaSNi0iOKkigqMFvkTKlGUYlD8GW7nNOYRrevuA46iTypPyv+06V3fEmvvazfntkBU34L0azAw==",
|
"integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/code-frame": "^7.28.6",
|
"@babel/code-frame": "^7.29.0",
|
||||||
"@babel/generator": "^7.28.6",
|
"@babel/generator": "^7.29.0",
|
||||||
"@babel/helper-compilation-targets": "^7.28.6",
|
"@babel/helper-compilation-targets": "^7.28.6",
|
||||||
"@babel/helper-module-transforms": "^7.28.6",
|
"@babel/helper-module-transforms": "^7.28.6",
|
||||||
"@babel/helpers": "^7.28.6",
|
"@babel/helpers": "^7.28.6",
|
||||||
"@babel/parser": "^7.28.6",
|
"@babel/parser": "^7.29.0",
|
||||||
"@babel/template": "^7.28.6",
|
"@babel/template": "^7.28.6",
|
||||||
"@babel/traverse": "^7.28.6",
|
"@babel/traverse": "^7.29.0",
|
||||||
"@babel/types": "^7.28.6",
|
"@babel/types": "^7.29.0",
|
||||||
"@jridgewell/remapping": "^2.3.5",
|
"@jridgewell/remapping": "^2.3.5",
|
||||||
"convert-source-map": "^2.0.0",
|
"convert-source-map": "^2.0.0",
|
||||||
"debug": "^4.1.0",
|
"debug": "^4.1.0",
|
||||||
@@ -112,14 +112,14 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/generator": {
|
"node_modules/@babel/generator": {
|
||||||
"version": "7.28.6",
|
"version": "7.29.1",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.6.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz",
|
||||||
"integrity": "sha512-lOoVRwADj8hjf7al89tvQ2a1lf53Z+7tiXMgpZJL3maQPDxh0DgLMN62B2MKUOFcoodBHLMbDM6WAbKgNy5Suw==",
|
"integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/parser": "^7.28.6",
|
"@babel/parser": "^7.29.0",
|
||||||
"@babel/types": "^7.28.6",
|
"@babel/types": "^7.29.0",
|
||||||
"@jridgewell/gen-mapping": "^0.3.12",
|
"@jridgewell/gen-mapping": "^0.3.12",
|
||||||
"@jridgewell/trace-mapping": "^0.3.28",
|
"@jridgewell/trace-mapping": "^0.3.28",
|
||||||
"jsesc": "^3.0.2"
|
"jsesc": "^3.0.2"
|
||||||
@@ -242,13 +242,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/parser": {
|
"node_modules/@babel/parser": {
|
||||||
"version": "7.28.6",
|
"version": "7.29.0",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.6.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz",
|
||||||
"integrity": "sha512-TeR9zWR18BvbfPmGbLampPMW+uW1NZnJlRuuHso8i87QZNq2JRF9i6RgxRqtEq+wQGsS19NNTWr2duhnE49mfQ==",
|
"integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/types": "^7.28.6"
|
"@babel/types": "^7.29.0"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"parser": "bin/babel-parser.js"
|
"parser": "bin/babel-parser.js"
|
||||||
@@ -305,18 +305,18 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/traverse": {
|
"node_modules/@babel/traverse": {
|
||||||
"version": "7.28.6",
|
"version": "7.29.0",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.6.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz",
|
||||||
"integrity": "sha512-fgWX62k02qtjqdSNTAGxmKYY/7FSL9WAS1o2Hu5+I5m9T0yxZzr4cnrfXQ/MX0rIifthCSs6FKTlzYbJcPtMNg==",
|
"integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/code-frame": "^7.28.6",
|
"@babel/code-frame": "^7.29.0",
|
||||||
"@babel/generator": "^7.28.6",
|
"@babel/generator": "^7.29.0",
|
||||||
"@babel/helper-globals": "^7.28.0",
|
"@babel/helper-globals": "^7.28.0",
|
||||||
"@babel/parser": "^7.28.6",
|
"@babel/parser": "^7.29.0",
|
||||||
"@babel/template": "^7.28.6",
|
"@babel/template": "^7.28.6",
|
||||||
"@babel/types": "^7.28.6",
|
"@babel/types": "^7.29.0",
|
||||||
"debug": "^4.3.1"
|
"debug": "^4.3.1"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -324,9 +324,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/types": {
|
"node_modules/@babel/types": {
|
||||||
"version": "7.28.6",
|
"version": "7.29.0",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.6.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz",
|
||||||
"integrity": "sha512-0ZrskXVEHSWIqZM/sQZ4EV3jZJXRkio/WCxaqKZP1g//CEWEPSfeZFcms4XeKBCHU0ZKnIkdJeU/kF+eRp5lBg==",
|
"integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -661,13 +661,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@playwright/test": {
|
"node_modules/@playwright/test": {
|
||||||
"version": "1.58.0",
|
"version": "1.58.2",
|
||||||
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.58.0.tgz",
|
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.58.2.tgz",
|
||||||
"integrity": "sha512-fWza+Lpbj6SkQKCrU6si4iu+fD2dD3gxNHFhUPxsfXBPhnv3rRSQVd0NtBUT9Z/RhF/boCBcuUaMUSTRTopjZg==",
|
"integrity": "sha512-akea+6bHYBBfA9uQqSYmlJXn61cTa+jbO87xVLCWbTqbWadRVmhxlXATaOjOgcBaWU4ePo0wB41KMFv3o35IXA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"playwright": "1.58.0"
|
"playwright": "1.58.2"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"playwright": "cli.js"
|
"playwright": "cli.js"
|
||||||
@@ -1363,9 +1363,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/pluginutils": {
|
"node_modules/@rolldown/pluginutils": {
|
||||||
"version": "1.0.0-beta.53",
|
"version": "1.0.0-rc.3",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.53.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.3.tgz",
|
||||||
"integrity": "sha512-vENRlFU4YbrwVqNDZ7fLvy+JR1CRkyr01jhSiDpE1u6py3OMzQfztQU2jxykW3ALNxO4kSlqIDeYyD0Y9RcQeQ==",
|
"integrity": "sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
@@ -1380,9 +1380,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@tanstack/query-devtools": {
|
"node_modules/@tanstack/query-devtools": {
|
||||||
"version": "5.92.0",
|
"version": "5.93.0",
|
||||||
"resolved": "https://registry.npmjs.org/@tanstack/query-devtools/-/query-devtools-5.92.0.tgz",
|
"resolved": "https://registry.npmjs.org/@tanstack/query-devtools/-/query-devtools-5.93.0.tgz",
|
||||||
"integrity": "sha512-N8D27KH1vEpVacvZgJL27xC6yPFUy0Zkezn5gnB3L3gRCxlDeSuiya7fKge8Y91uMTnC8aSxBQhcK6ocY7alpQ==",
|
"integrity": "sha512-+kpsx1NQnOFTZsw6HAFCW3HkKg0+2cepGtAWXjiiSOJJ1CtQpt72EE2nyZb+AjAbLRPoeRmPJ8MtQd8r8gsPdg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"funding": {
|
"funding": {
|
||||||
"type": "github",
|
"type": "github",
|
||||||
@@ -1390,9 +1390,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@tanstack/react-query": {
|
"node_modules/@tanstack/react-query": {
|
||||||
"version": "5.90.20",
|
"version": "5.90.21",
|
||||||
"resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.90.20.tgz",
|
"resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.90.21.tgz",
|
||||||
"integrity": "sha512-vXBxa+qeyveVO7OA0jX1z+DeyCA4JKnThKv411jd5SORpBKgkcVnYKCiBgECvADvniBX7tobwBmg01qq9JmMJw==",
|
"integrity": "sha512-0Lu6y5t+tvlTJMTO7oh5NSpJfpg/5D41LlThfepTixPYkJ0sE2Jj0m0f6yYqujBwIXlId87e234+MxG3D3g7kg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tanstack/query-core": "5.90.20"
|
"@tanstack/query-core": "5.90.20"
|
||||||
@@ -1406,19 +1406,19 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@tanstack/react-query-devtools": {
|
"node_modules/@tanstack/react-query-devtools": {
|
||||||
"version": "5.91.2",
|
"version": "5.91.3",
|
||||||
"resolved": "https://registry.npmjs.org/@tanstack/react-query-devtools/-/react-query-devtools-5.91.2.tgz",
|
"resolved": "https://registry.npmjs.org/@tanstack/react-query-devtools/-/react-query-devtools-5.91.3.tgz",
|
||||||
"integrity": "sha512-ZJ1503ay5fFeEYFUdo7LMNFzZryi6B0Cacrgr2h1JRkvikK1khgIq6Nq2EcblqEdIlgB/r7XDW8f8DQ89RuUgg==",
|
"integrity": "sha512-nlahjMtd/J1h7IzOOfqeyDh5LNfG0eULwlltPEonYy0QL+nqrBB+nyzJfULV+moL7sZyxc2sHdNJki+vLA9BSA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tanstack/query-devtools": "5.92.0"
|
"@tanstack/query-devtools": "5.93.0"
|
||||||
},
|
},
|
||||||
"funding": {
|
"funding": {
|
||||||
"type": "github",
|
"type": "github",
|
||||||
"url": "https://github.com/sponsors/tannerlinsley"
|
"url": "https://github.com/sponsors/tannerlinsley"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@tanstack/react-query": "^5.90.14",
|
"@tanstack/react-query": "^5.90.20",
|
||||||
"react": "^18 || ^19"
|
"react": "^18 || ^19"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -1479,9 +1479,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@types/node": {
|
"node_modules/@types/node": {
|
||||||
"version": "24.10.9",
|
"version": "24.10.13",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.9.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.13.tgz",
|
||||||
"integrity": "sha512-ne4A0IpG3+2ETuREInjPNhUGis1SFjv1d5asp8MzEAGtOZeTeHVDOYqOgqfhvseqg/iXty2hjBf1zAOb7RNiNw==",
|
"integrity": "sha512-oH72nZRfDv9lADUBSo104Aq7gPHpQZc4BTx38r9xf9pg5LfP6EzSyH2n7qFmmxRQXh7YlUXODcYsg6PuTDSxGg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -1489,9 +1489,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@types/react": {
|
"node_modules/@types/react": {
|
||||||
"version": "19.2.9",
|
"version": "19.2.14",
|
||||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.9.tgz",
|
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz",
|
||||||
"integrity": "sha512-Lpo8kgb/igvMIPeNV2rsYKTgaORYdO1XGVZ4Qz3akwOj0ySGYMPlQWa8BaLn0G63D1aSaAQ5ldR06wCpChQCjA==",
|
"integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -1509,16 +1509,16 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@vitejs/plugin-react": {
|
"node_modules/@vitejs/plugin-react": {
|
||||||
"version": "5.1.2",
|
"version": "5.1.4",
|
||||||
"resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.1.4.tgz",
|
||||||
"integrity": "sha512-EcA07pHJouywpzsoTUqNh5NwGayl2PPVEJKUSinGGSxFGYn+shYbqMGBg6FXDqgXum9Ou/ecb+411ssw8HImJQ==",
|
"integrity": "sha512-VIcFLdRi/VYRU8OL/puL7QXMYafHmqOnwTZY50U1JPlCNj30PxCMx65c494b1K9be9hX83KVt0+gTEwTWLqToA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/core": "^7.28.5",
|
"@babel/core": "^7.29.0",
|
||||||
"@babel/plugin-transform-react-jsx-self": "^7.27.1",
|
"@babel/plugin-transform-react-jsx-self": "^7.27.1",
|
||||||
"@babel/plugin-transform-react-jsx-source": "^7.27.1",
|
"@babel/plugin-transform-react-jsx-source": "^7.27.1",
|
||||||
"@rolldown/pluginutils": "1.0.0-beta.53",
|
"@rolldown/pluginutils": "1.0.0-rc.3",
|
||||||
"@types/babel__core": "^7.20.5",
|
"@types/babel__core": "^7.20.5",
|
||||||
"react-refresh": "^0.18.0"
|
"react-refresh": "^0.18.0"
|
||||||
},
|
},
|
||||||
@@ -1550,19 +1550,6 @@
|
|||||||
"node": ">= 8"
|
"node": ">= 8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/anymatch/node_modules/picomatch": {
|
|
||||||
"version": "2.3.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
|
|
||||||
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=8.6"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/jonschlinkert"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/arg": {
|
"node_modules/arg": {
|
||||||
"version": "5.0.2",
|
"version": "5.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz",
|
||||||
@@ -1577,9 +1564,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/autoprefixer": {
|
"node_modules/autoprefixer": {
|
||||||
"version": "10.4.23",
|
"version": "10.4.24",
|
||||||
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.23.tgz",
|
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.24.tgz",
|
||||||
"integrity": "sha512-YYTXSFulfwytnjAPlw8QHncHJmlvFKtczb8InXaAx9Q0LbfDnfEYDE55omerIJKihhmU61Ft+cAOSzQVaBUmeA==",
|
"integrity": "sha512-uHZg7N9ULTVbutaIsDRoUkoS8/h3bdsmVJYZ5l3wv8Cp/6UIIoRDm90hZ+BwxUj/hGBEzLxdHNSKuFpn8WOyZw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
@@ -1598,7 +1585,7 @@
|
|||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"browserslist": "^4.28.1",
|
"browserslist": "^4.28.1",
|
||||||
"caniuse-lite": "^1.0.30001760",
|
"caniuse-lite": "^1.0.30001766",
|
||||||
"fraction.js": "^5.3.4",
|
"fraction.js": "^5.3.4",
|
||||||
"picocolors": "^1.1.1",
|
"picocolors": "^1.1.1",
|
||||||
"postcss-value-parser": "^4.2.0"
|
"postcss-value-parser": "^4.2.0"
|
||||||
@@ -1614,24 +1601,27 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/axios": {
|
"node_modules/axios": {
|
||||||
"version": "1.13.3",
|
"version": "1.13.5",
|
||||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.13.3.tgz",
|
"resolved": "https://registry.npmjs.org/axios/-/axios-1.13.5.tgz",
|
||||||
"integrity": "sha512-ERT8kdX7DZjtUm7IitEyV7InTHAF42iJuMArIiDIV5YtPanJkgw4hw5Dyg9fh0mihdWNn1GKaeIWErfe56UQ1g==",
|
"integrity": "sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"follow-redirects": "^1.15.6",
|
"follow-redirects": "^1.15.11",
|
||||||
"form-data": "^4.0.4",
|
"form-data": "^4.0.5",
|
||||||
"proxy-from-env": "^1.1.0"
|
"proxy-from-env": "^1.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/baseline-browser-mapping": {
|
"node_modules/baseline-browser-mapping": {
|
||||||
"version": "2.9.18",
|
"version": "2.10.0",
|
||||||
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.18.tgz",
|
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.0.tgz",
|
||||||
"integrity": "sha512-e23vBV1ZLfjb9apvfPk4rHVu2ry6RIr2Wfs+O324okSidrX7pTAnEJPCh/O5BtRlr7QtZI7ktOP3vsqr7Z5XoA==",
|
"integrity": "sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"bin": {
|
"bin": {
|
||||||
"baseline-browser-mapping": "dist/cli.js"
|
"baseline-browser-mapping": "dist/cli.cjs"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/binary-extensions": {
|
"node_modules/binary-extensions": {
|
||||||
@@ -1718,9 +1708,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/caniuse-lite": {
|
"node_modules/caniuse-lite": {
|
||||||
"version": "1.0.30001766",
|
"version": "1.0.30001774",
|
||||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001766.tgz",
|
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001774.tgz",
|
||||||
"integrity": "sha512-4C0lfJ0/YPjJQHagaE9x2Elb69CIqEPZeG0anQt9SIvIoOH4a4uaRl73IavyO+0qZh6MDLH//DrXThEYKHkmYA==",
|
"integrity": "sha512-DDdwPGz99nmIEv216hKSgLD+D4ikHQHjBC/seF98N9CPqRX4M5mSxT9eTV6oyisnJcuzxtZy4n17yKKQYmYQOA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
@@ -1912,9 +1902,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/electron-to-chromium": {
|
"node_modules/electron-to-chromium": {
|
||||||
"version": "1.5.278",
|
"version": "1.5.302",
|
||||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.278.tgz",
|
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.302.tgz",
|
||||||
"integrity": "sha512-dQ0tM1svDRQOwxnXxm+twlGTjr9Upvt8UFWAgmLsxEzFQxhbti4VwxmMjsDxVC51Zo84swW7FVCXEV+VAkhuPw==",
|
"integrity": "sha512-sM6HAN2LyK82IyPBpznDRqlTQAtuSaO+ShzFiWTvoMJLHyZ+Y39r8VMfHzwbU8MVBzQ4Wdn85+wlZl2TLGIlwg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
@@ -2013,24 +2003,6 @@
|
|||||||
"reusify": "^1.0.4"
|
"reusify": "^1.0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/fdir": {
|
|
||||||
"version": "6.5.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
|
|
||||||
"integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=12.0.0"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"picomatch": "^3 || ^4"
|
|
||||||
},
|
|
||||||
"peerDependenciesMeta": {
|
|
||||||
"picomatch": {
|
|
||||||
"optional": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/fill-range": {
|
"node_modules/fill-range": {
|
||||||
"version": "7.1.1",
|
"version": "7.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
|
||||||
@@ -2095,9 +2067,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/fsevents": {
|
"node_modules/fsevents": {
|
||||||
"version": "2.3.3",
|
"version": "2.3.2",
|
||||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
|
||||||
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
|
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@@ -2676,19 +2648,6 @@
|
|||||||
"node": ">=8.6"
|
"node": ">=8.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/micromatch/node_modules/picomatch": {
|
|
||||||
"version": "2.3.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
|
|
||||||
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=8.6"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/jonschlinkert"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/mime-db": {
|
"node_modules/mime-db": {
|
||||||
"version": "1.52.0",
|
"version": "1.52.0",
|
||||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
|
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
|
||||||
@@ -2812,13 +2771,13 @@
|
|||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
"node_modules/picomatch": {
|
"node_modules/picomatch": {
|
||||||
"version": "4.0.3",
|
"version": "2.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
|
||||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=8.6"
|
||||||
},
|
},
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/jonschlinkert"
|
"url": "https://github.com/sponsors/jonschlinkert"
|
||||||
@@ -2845,13 +2804,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/playwright": {
|
"node_modules/playwright": {
|
||||||
"version": "1.58.0",
|
"version": "1.58.2",
|
||||||
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.58.0.tgz",
|
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.58.2.tgz",
|
||||||
"integrity": "sha512-2SVA0sbPktiIY/MCOPX8e86ehA/e+tDNq+e5Y8qjKYti2Z/JG7xnronT/TXTIkKbYGWlCbuucZ6dziEgkoEjQQ==",
|
"integrity": "sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"playwright-core": "1.58.0"
|
"playwright-core": "1.58.2"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"playwright": "cli.js"
|
"playwright": "cli.js"
|
||||||
@@ -2864,9 +2823,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/playwright-core": {
|
"node_modules/playwright-core": {
|
||||||
"version": "1.58.0",
|
"version": "1.58.2",
|
||||||
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.58.0.tgz",
|
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.58.2.tgz",
|
||||||
"integrity": "sha512-aaoB1RWrdNi3//rOeKuMiS65UCcgOVljU46At6eFcOFPFHWtd2weHRRow6z/n+Lec0Lvu0k9ZPKJSjPugikirw==",
|
"integrity": "sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"bin": {
|
"bin": {
|
||||||
@@ -2876,21 +2835,6 @@
|
|||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/playwright/node_modules/fsevents": {
|
|
||||||
"version": "2.3.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
|
|
||||||
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
|
|
||||||
"dev": true,
|
|
||||||
"hasInstallScript": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"darwin"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/postcss": {
|
"node_modules/postcss": {
|
||||||
"version": "8.5.6",
|
"version": "8.5.6",
|
||||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
|
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
|
||||||
@@ -3082,30 +3026,30 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/react": {
|
"node_modules/react": {
|
||||||
"version": "19.2.3",
|
"version": "19.2.4",
|
||||||
"resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz",
|
||||||
"integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==",
|
"integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/react-dom": {
|
"node_modules/react-dom": {
|
||||||
"version": "19.2.3",
|
"version": "19.2.4",
|
||||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz",
|
||||||
"integrity": "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==",
|
"integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"scheduler": "^0.27.0"
|
"scheduler": "^0.27.0"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"react": "^19.2.3"
|
"react": "^19.2.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/react-hook-form": {
|
"node_modules/react-hook-form": {
|
||||||
"version": "7.71.1",
|
"version": "7.71.2",
|
||||||
"resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.71.1.tgz",
|
"resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.71.2.tgz",
|
||||||
"integrity": "sha512-9SUJKCGKo8HUSsCO+y0CtqkqI5nNuaDqTxyqPsZPqIwudpj4rCrAz/jZV+jn57bx5gtZKOh3neQu94DXMc+w5w==",
|
"integrity": "sha512-1CHvcDYzuRUNOflt4MOq3ZM46AronNJtQ1S7tnX6YN4y72qhgiUItpacZUAQ0TyWYci3yz1X+rXaSxiuEm86PA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18.0.0"
|
"node": ">=18.0.0"
|
||||||
@@ -3196,19 +3140,6 @@
|
|||||||
"node": ">=8.10.0"
|
"node": ">=8.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/readdirp/node_modules/picomatch": {
|
|
||||||
"version": "2.3.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
|
|
||||||
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=8.6"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/jonschlinkert"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/resolve": {
|
"node_modules/resolve": {
|
||||||
"version": "1.22.11",
|
"version": "1.22.11",
|
||||||
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz",
|
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz",
|
||||||
@@ -3368,9 +3299,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/tailwind-merge": {
|
"node_modules/tailwind-merge": {
|
||||||
"version": "3.4.0",
|
"version": "3.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.5.0.tgz",
|
||||||
"integrity": "sha512-uSaO4gnW+b3Y2aWoWfFpX62vn2sR3skfhbjsEnaBI81WD1wBLlHZe5sWf0AqjksNdYTbGBEd0UasQMT3SNV15g==",
|
"integrity": "sha512-I8K9wewnVDkL1NTGoqWmVEIlUcB9gFriAEkXkfCjX5ib8ezGxtR3xD7iZIxrfArjEsH7F1CHD4RFUtxefdqV/A==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"funding": {
|
"funding": {
|
||||||
"type": "github",
|
"type": "github",
|
||||||
@@ -3465,6 +3396,37 @@
|
|||||||
"url": "https://github.com/sponsors/SuperchupuDev"
|
"url": "https://github.com/sponsors/SuperchupuDev"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/tinyglobby/node_modules/fdir": {
|
||||||
|
"version": "6.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
|
||||||
|
"integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"picomatch": "^3 || ^4"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"picomatch": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/tinyglobby/node_modules/picomatch": {
|
||||||
|
"version": "4.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
||||||
|
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/jonschlinkert"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/to-regex-range": {
|
"node_modules/to-regex-range": {
|
||||||
"version": "5.0.1",
|
"version": "5.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
|
||||||
@@ -3638,6 +3600,52 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/vite/node_modules/fdir": {
|
||||||
|
"version": "6.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
|
||||||
|
"integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"picomatch": "^3 || ^4"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"picomatch": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/vite/node_modules/fsevents": {
|
||||||
|
"version": "2.3.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
||||||
|
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
|
||||||
|
"dev": true,
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/vite/node_modules/picomatch": {
|
||||||
|
"version": "4.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
||||||
|
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/jonschlinkert"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/yallist": {
|
"node_modules/yallist": {
|
||||||
"version": "3.1.1",
|
"version": "3.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ function ClientGeneralPage() {
|
|||||||
const [name, setName] = useState("");
|
const [name, setName] = useState("");
|
||||||
const [description, setDescription] = useState("");
|
const [description, setDescription] = useState("");
|
||||||
const [logoUrl, setLogoUrl] = useState("");
|
const [logoUrl, setLogoUrl] = useState("");
|
||||||
const [clientType, setClientType] = useState<ClientType>("confidential");
|
const [clientType, setClientType] = useState<ClientType>("private");
|
||||||
const [status, setStatus] = useState<ClientStatus>("active");
|
const [status, setStatus] = useState<ClientStatus>("active");
|
||||||
const [redirectUris, setRedirectUris] = useState("");
|
const [redirectUris, setRedirectUris] = useState("");
|
||||||
const [scopes, setScopes] = useState<ScopeItem[]>(() => [
|
const [scopes, setScopes] = useState<ScopeItem[]>(() => [
|
||||||
@@ -157,6 +157,21 @@ function ClientGeneralPage() {
|
|||||||
}
|
}
|
||||||
alert(t("msg.dev.clients.general.saved", "설정이 저장되었습니다."));
|
alert(t("msg.dev.clients.general.saved", "설정이 저장되었습니다."));
|
||||||
},
|
},
|
||||||
|
onError: (err) => {
|
||||||
|
const errorMessage =
|
||||||
|
(err as AxiosError<{ error?: string }>).response?.data?.error ??
|
||||||
|
(err as Error)?.message ??
|
||||||
|
t("msg.common.unknown_error", "unknown error");
|
||||||
|
alert(
|
||||||
|
t(
|
||||||
|
"msg.dev.clients.general.save_error",
|
||||||
|
"저장에 실패했습니다: {{error}}",
|
||||||
|
{
|
||||||
|
error: errorMessage,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!isCreate && isLoading) {
|
if (!isCreate && isLoading) {
|
||||||
@@ -490,7 +505,7 @@ function ClientGeneralPage() {
|
|||||||
<label
|
<label
|
||||||
className={cn(
|
className={cn(
|
||||||
"relative flex cursor-pointer flex-col gap-1 rounded-xl border-2 p-4 transition",
|
"relative flex cursor-pointer flex-col gap-1 rounded-xl border-2 p-4 transition",
|
||||||
clientType === "confidential"
|
clientType === "private"
|
||||||
? "border-primary bg-primary/5"
|
? "border-primary bg-primary/5"
|
||||||
: "border-border bg-card hover:border-muted-foreground/40",
|
: "border-border bg-card hover:border-muted-foreground/40",
|
||||||
)}
|
)}
|
||||||
@@ -499,31 +514,28 @@ function ClientGeneralPage() {
|
|||||||
className="sr-only"
|
className="sr-only"
|
||||||
type="radio"
|
type="radio"
|
||||||
name="client-type"
|
name="client-type"
|
||||||
checked={clientType === "confidential"}
|
checked={clientType === "private"}
|
||||||
onChange={() => setClientType("confidential")}
|
onChange={() => setClientType("private")}
|
||||||
/>
|
/>
|
||||||
<span className="flex items-center gap-2 text-sm font-bold uppercase text-foreground">
|
<span className="flex items-center gap-2 text-sm font-bold uppercase text-foreground">
|
||||||
<Shield className="h-4 w-4 text-primary" />
|
<Shield className="h-4 w-4 text-primary" />
|
||||||
{t(
|
{t("ui.dev.clients.general.security.private", "Private")}
|
||||||
"ui.dev.clients.general.security.confidential",
|
|
||||||
"Confidential",
|
|
||||||
)}
|
|
||||||
</span>
|
</span>
|
||||||
<span className="text-xs text-muted-foreground">
|
<span className="text-xs text-muted-foreground">
|
||||||
{t(
|
{t(
|
||||||
"msg.dev.clients.general.security.confidential_help",
|
"msg.dev.clients.general.security.private_help",
|
||||||
"서버 사이드 앱(예: Node.js, Java)처럼 비밀키를 안전하게 보관 가능한 경우.",
|
"서버 사이드 앱(예: Node.js, Java)처럼 비밀키를 안전하게 보관 가능한 경우.",
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
<span className="absolute right-4 top-4 text-primary">
|
<span className="absolute right-4 top-4 text-primary">
|
||||||
{clientType === "confidential" ? "✓" : ""}
|
{clientType === "private" ? "✓" : ""}
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<label
|
<label
|
||||||
className={cn(
|
className={cn(
|
||||||
"relative flex cursor-pointer flex-col gap-1 rounded-xl border-2 p-4 transition",
|
"relative flex cursor-pointer flex-col gap-1 rounded-xl border-2 p-4 transition",
|
||||||
clientType === "public"
|
clientType === "pkce"
|
||||||
? "border-primary bg-primary/5"
|
? "border-primary bg-primary/5"
|
||||||
: "border-border bg-card hover:border-muted-foreground/40",
|
: "border-border bg-card hover:border-muted-foreground/40",
|
||||||
)}
|
)}
|
||||||
@@ -532,21 +544,21 @@ function ClientGeneralPage() {
|
|||||||
className="sr-only"
|
className="sr-only"
|
||||||
type="radio"
|
type="radio"
|
||||||
name="client-type"
|
name="client-type"
|
||||||
checked={clientType === "public"}
|
checked={clientType === "pkce"}
|
||||||
onChange={() => setClientType("public")}
|
onChange={() => setClientType("pkce")}
|
||||||
/>
|
/>
|
||||||
<span className="flex items-center gap-2 text-sm font-bold uppercase text-foreground">
|
<span className="flex items-center gap-2 text-sm font-bold uppercase text-foreground">
|
||||||
<Sparkles className="h-4 w-4" />
|
<Sparkles className="h-4 w-4" />
|
||||||
{t("ui.dev.clients.general.security.public", "Public")}
|
{t("ui.dev.clients.general.security.pkce", "PKCE")}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-xs text-muted-foreground">
|
<span className="text-xs text-muted-foreground">
|
||||||
{t(
|
{t(
|
||||||
"msg.dev.clients.general.security.public_help",
|
"msg.dev.clients.general.security.pkce_help",
|
||||||
"SPA/모바일 앱처럼 비밀키 보관이 어려운 경우. PKCE를 기본 사용합니다.",
|
"SPA/모바일 앱처럼 비밀키 보관이 어려운 경우. PKCE를 기본 사용합니다.",
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
<span className="absolute right-4 top-4 text-primary">
|
<span className="absolute right-4 top-4 text-primary">
|
||||||
{clientType === "public" ? "✓" : ""}
|
{clientType === "pkce" ? "✓" : ""}
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -87,7 +87,10 @@ function ClientsPage() {
|
|||||||
|
|
||||||
const clients = data?.items || [];
|
const clients = data?.items || [];
|
||||||
const totalClients = clients.length;
|
const totalClients = clients.length;
|
||||||
// TODO: Add real stats for active sessions and auth failures
|
const activeClients = clients.filter(
|
||||||
|
(client) => client.status === "active",
|
||||||
|
).length;
|
||||||
|
// TODO: Replace with real session/auth-failure metrics when backend endpoints are available.
|
||||||
type StatTone = "up" | "down" | "stable";
|
type StatTone = "up" | "down" | "stable";
|
||||||
type StatItem = {
|
type StatItem = {
|
||||||
labelKey: string;
|
labelKey: string;
|
||||||
@@ -110,10 +113,10 @@ function ClientsPage() {
|
|||||||
{
|
{
|
||||||
labelKey: "ui.dev.clients.stats.active_sessions",
|
labelKey: "ui.dev.clients.stats.active_sessions",
|
||||||
labelFallback: "활성 세션",
|
labelFallback: "활성 세션",
|
||||||
value: "-",
|
value: activeClients.toString(),
|
||||||
deltaKey: "ui.dev.clients.stats.not_impl",
|
deltaKey: "ui.dev.clients.stats.realtime",
|
||||||
deltaFallback: "Not impl",
|
deltaFallback: "Realtime",
|
||||||
tone: "stable" as const,
|
tone: "up" as const,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
labelKey: "ui.dev.clients.stats.auth_failures",
|
labelKey: "ui.dev.clients.stats.auth_failures",
|
||||||
@@ -266,7 +269,7 @@ function ClientsPage() {
|
|||||||
<TableCell>
|
<TableCell>
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<div className="flex h-9 w-9 items-center justify-center rounded-lg bg-primary/10 text-primary">
|
<div className="flex h-9 w-9 items-center justify-center rounded-lg bg-primary/10 text-primary">
|
||||||
{client.type === "confidential" ? (
|
{client.type === "private" ? (
|
||||||
<ServerCog className="h-4 w-4" />
|
<ServerCog className="h-4 w-4" />
|
||||||
) : (
|
) : (
|
||||||
<ShieldHalf className="h-4 w-4" />
|
<ShieldHalf className="h-4 w-4" />
|
||||||
@@ -309,16 +312,11 @@ function ClientsPage() {
|
|||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<Badge
|
<Badge
|
||||||
variant={
|
variant={client.type === "private" ? "success" : "muted"}
|
||||||
client.type === "confidential" ? "success" : "muted"
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
{client.type === "confidential"
|
{client.type === "private"
|
||||||
? t(
|
? t("ui.dev.clients.type.private", "Private")
|
||||||
"ui.dev.clients.type.confidential",
|
: t("ui.dev.clients.type.pkce", "PKCE")}
|
||||||
"기밀(Confidential)",
|
|
||||||
)
|
|
||||||
: t("ui.dev.clients.type.public", "Public")}
|
|
||||||
</Badge>
|
</Badge>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import apiClient from "./apiClient";
|
import apiClient from "./apiClient";
|
||||||
|
|
||||||
export type ClientStatus = "active" | "inactive";
|
export type ClientStatus = "active" | "inactive";
|
||||||
export type ClientType = "confidential" | "public";
|
export type ClientType = "private" | "pkce";
|
||||||
|
|
||||||
export type ClientSummary = {
|
export type ClientSummary = {
|
||||||
id: string;
|
id: string;
|
||||||
|
|||||||
@@ -261,9 +261,9 @@ empty = "Empty"
|
|||||||
subtitle = "Subtitle"
|
subtitle = "Subtitle"
|
||||||
|
|
||||||
[msg.dev.clients.general.security]
|
[msg.dev.clients.general.security]
|
||||||
confidential_help = "Confidential Help"
|
private_help = "Private App (Server-side): For apps that can safely store a client secret, such as Node.js or Java servers."
|
||||||
public_help = "Public Help"
|
pkce_help = "PKCE App (SPA/Mobile): For apps that cannot safely store a client secret. PKCE is mandatory."
|
||||||
subtitle = "Subtitle"
|
subtitle = "Select application type. Security level determines authentication method."
|
||||||
|
|
||||||
[msg.dev.clients.help]
|
[msg.dev.clients.help]
|
||||||
docs_body = "Includes PKCE, client_secret_basic, redirect URI validation tips."
|
docs_body = "Includes PKCE, client_secret_basic, redirect URI validation tips."
|
||||||
@@ -1051,8 +1051,8 @@ name = "Scope Name"
|
|||||||
delete = "Delete"
|
delete = "Delete"
|
||||||
|
|
||||||
[ui.dev.clients.general.security]
|
[ui.dev.clients.general.security]
|
||||||
confidential = "Confidential"
|
private = "Private"
|
||||||
public = "Public"
|
pkce = "PKCE"
|
||||||
title = "Security Settings"
|
title = "Security Settings"
|
||||||
|
|
||||||
[ui.dev.clients.help]
|
[ui.dev.clients.help]
|
||||||
@@ -1085,8 +1085,8 @@ status = "Status"
|
|||||||
type = "Type"
|
type = "Type"
|
||||||
|
|
||||||
[ui.dev.clients.type]
|
[ui.dev.clients.type]
|
||||||
confidential = "Confidential"
|
private = "Private"
|
||||||
public = "Public"
|
pkce = "PKCE"
|
||||||
|
|
||||||
[ui.dev.dashboard]
|
[ui.dev.dashboard]
|
||||||
ready_badge = "devfront ready"
|
ready_badge = "devfront ready"
|
||||||
|
|||||||
@@ -261,8 +261,8 @@ empty = "등록된 스코프가 없습니다."
|
|||||||
subtitle = "이 앱이 요청할 수 있는 권한 범위를 정의합니다."
|
subtitle = "이 앱이 요청할 수 있는 권한 범위를 정의합니다."
|
||||||
|
|
||||||
[msg.dev.clients.general.security]
|
[msg.dev.clients.general.security]
|
||||||
confidential_help = "서버 사이드 앱(예: Node.js, Java)처럼 비밀키를 안전하게 보관 가능한 경우."
|
private_help = "Private 앱 (서버 사이드 앱): Node.js, Java 등 비밀키를 안전하게 보관 가능한 경우 사용합니다."
|
||||||
public_help = "SPA/모바일 앱처럼 비밀키 보관이 어려운 경우. PKCE를 기본 사용합니다."
|
pkce_help = "PKCE 앱 (SPA/모바일): 브라우저나 앱처럼 비밀키를 보관하기 어려운 경우 사용하며, PKCE가 강제됩니다."
|
||||||
subtitle = "앱 유형을 선택하세요. 보안 수준에 따라 인증 방식이 달라집니다."
|
subtitle = "앱 유형을 선택하세요. 보안 수준에 따라 인증 방식이 달라집니다."
|
||||||
|
|
||||||
[msg.dev.clients.help]
|
[msg.dev.clients.help]
|
||||||
@@ -1051,8 +1051,8 @@ name = "Scope Name"
|
|||||||
delete = "Delete"
|
delete = "Delete"
|
||||||
|
|
||||||
[ui.dev.clients.general.security]
|
[ui.dev.clients.general.security]
|
||||||
confidential = "Confidential"
|
private = "Private"
|
||||||
public = "Public"
|
pkce = "PKCE"
|
||||||
title = "보안 설정"
|
title = "보안 설정"
|
||||||
|
|
||||||
[ui.dev.clients.help]
|
[ui.dev.clients.help]
|
||||||
@@ -1085,8 +1085,8 @@ status = "상태"
|
|||||||
type = "유형"
|
type = "유형"
|
||||||
|
|
||||||
[ui.dev.clients.type]
|
[ui.dev.clients.type]
|
||||||
confidential = "기밀(Confidential)"
|
private = "Private"
|
||||||
public = "Public"
|
pkce = "PKCE"
|
||||||
|
|
||||||
[ui.dev.dashboard]
|
[ui.dev.dashboard]
|
||||||
ready_badge = "devfront ready"
|
ready_badge = "devfront ready"
|
||||||
|
|||||||
@@ -261,8 +261,8 @@ empty = ""
|
|||||||
subtitle = ""
|
subtitle = ""
|
||||||
|
|
||||||
[msg.dev.clients.general.security]
|
[msg.dev.clients.general.security]
|
||||||
confidential_help = ""
|
private_help = ""
|
||||||
public_help = ""
|
pkce_help = ""
|
||||||
subtitle = ""
|
subtitle = ""
|
||||||
|
|
||||||
[msg.dev.clients.help]
|
[msg.dev.clients.help]
|
||||||
@@ -1063,8 +1063,8 @@ name = ""
|
|||||||
delete = ""
|
delete = ""
|
||||||
|
|
||||||
[ui.dev.clients.general.security]
|
[ui.dev.clients.general.security]
|
||||||
confidential = ""
|
private = ""
|
||||||
public = ""
|
pkce = ""
|
||||||
title = ""
|
title = ""
|
||||||
|
|
||||||
[ui.dev.clients.help]
|
[ui.dev.clients.help]
|
||||||
@@ -1097,8 +1097,8 @@ status = ""
|
|||||||
type = ""
|
type = ""
|
||||||
|
|
||||||
[ui.dev.clients.type]
|
[ui.dev.clients.type]
|
||||||
confidential = ""
|
private = ""
|
||||||
public = ""
|
pkce = ""
|
||||||
|
|
||||||
[ui.dev.dashboard]
|
[ui.dev.dashboard]
|
||||||
ready_badge = ""
|
ready_badge = ""
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ test("clients page loads correctly", async ({ page }) => {
|
|||||||
{
|
{
|
||||||
id: "client-playwright",
|
id: "client-playwright",
|
||||||
name: "Playwright Client",
|
name: "Playwright Client",
|
||||||
type: "confidential",
|
type: "private",
|
||||||
status: "active",
|
status: "active",
|
||||||
createdAt: new Date().toISOString(),
|
createdAt: new Date().toISOString(),
|
||||||
redirectUris: ["http://localhost:5174/callback"],
|
redirectUris: ["http://localhost:5174/callback"],
|
||||||
|
|||||||
@@ -248,6 +248,7 @@ note = "Note"
|
|||||||
load_error = "Error loading client: {{error}}"
|
load_error = "Error loading client: {{error}}"
|
||||||
loading = "Loading client..."
|
loading = "Loading client..."
|
||||||
saved = "Saved"
|
saved = "Saved"
|
||||||
|
save_error = "Failed to save: {{error}}"
|
||||||
|
|
||||||
[msg.dev.clients.general.identity]
|
[msg.dev.clients.general.identity]
|
||||||
logo_help = "Logo Help"
|
logo_help = "Logo Help"
|
||||||
@@ -261,9 +262,9 @@ empty = "Empty"
|
|||||||
subtitle = "Subtitle"
|
subtitle = "Subtitle"
|
||||||
|
|
||||||
[msg.dev.clients.general.security]
|
[msg.dev.clients.general.security]
|
||||||
confidential_help = "Confidential Help"
|
private_help = "Private App (Server-side): For apps that can safely store a client secret, such as Node.js or Java servers."
|
||||||
public_help = "Public Help"
|
pkce_help = "PKCE App (SPA/Mobile): For apps that cannot safely store a client secret. PKCE is mandatory."
|
||||||
subtitle = "Subtitle"
|
subtitle = "Select application type. Security level determines authentication method."
|
||||||
|
|
||||||
[msg.dev.clients.help]
|
[msg.dev.clients.help]
|
||||||
docs_body = "Includes PKCE, client_secret_basic, redirect URI validation tips."
|
docs_body = "Includes PKCE, client_secret_basic, redirect URI validation tips."
|
||||||
@@ -1051,8 +1052,8 @@ name = "Scope Name"
|
|||||||
delete = "Delete"
|
delete = "Delete"
|
||||||
|
|
||||||
[ui.dev.clients.general.security]
|
[ui.dev.clients.general.security]
|
||||||
confidential = "Confidential"
|
private = "Private"
|
||||||
public = "Public"
|
pkce = "PKCE"
|
||||||
title = "Security Settings"
|
title = "Security Settings"
|
||||||
|
|
||||||
[ui.dev.clients.help]
|
[ui.dev.clients.help]
|
||||||
@@ -1085,8 +1086,8 @@ status = "Status"
|
|||||||
type = "Type"
|
type = "Type"
|
||||||
|
|
||||||
[ui.dev.clients.type]
|
[ui.dev.clients.type]
|
||||||
confidential = "Confidential"
|
private = "Private"
|
||||||
public = "Public"
|
pkce = "PKCE"
|
||||||
|
|
||||||
[ui.dev.dashboard]
|
[ui.dev.dashboard]
|
||||||
ready_badge = "devfront ready"
|
ready_badge = "devfront ready"
|
||||||
|
|||||||
@@ -248,6 +248,7 @@ note = "엔드포인트는 읽기 전용으로 유지하고, 비밀키 재발행
|
|||||||
load_error = "Error loading client: {{error}}"
|
load_error = "Error loading client: {{error}}"
|
||||||
loading = "Loading client..."
|
loading = "Loading client..."
|
||||||
saved = "설정이 저장되었습니다."
|
saved = "설정이 저장되었습니다."
|
||||||
|
save_error = "저장 실패: {{error}}"
|
||||||
|
|
||||||
[msg.dev.clients.general.identity]
|
[msg.dev.clients.general.identity]
|
||||||
logo_help = "인증 화면에 표시될 PNG/SVG URL입니다."
|
logo_help = "인증 화면에 표시될 PNG/SVG URL입니다."
|
||||||
@@ -261,8 +262,8 @@ empty = "등록된 스코프가 없습니다."
|
|||||||
subtitle = "이 앱이 요청할 수 있는 권한 범위를 정의합니다."
|
subtitle = "이 앱이 요청할 수 있는 권한 범위를 정의합니다."
|
||||||
|
|
||||||
[msg.dev.clients.general.security]
|
[msg.dev.clients.general.security]
|
||||||
confidential_help = "서버 사이드 앱(예: Node.js, Java)처럼 비밀키를 안전하게 보관 가능한 경우."
|
private_help = "Private 앱 (서버 사이드 앱): Node.js, Java 등 비밀키를 안전하게 보관 가능한 경우 사용합니다."
|
||||||
public_help = "SPA/모바일 앱처럼 비밀키 보관이 어려운 경우. PKCE를 기본 사용합니다."
|
pkce_help = "PKCE 앱 (SPA/모바일): 브라우저나 앱처럼 비밀키를 보관하기 어려운 경우 사용하며, PKCE가 강제됩니다."
|
||||||
subtitle = "앱 유형을 선택하세요. 보안 수준에 따라 인증 방식이 달라집니다."
|
subtitle = "앱 유형을 선택하세요. 보안 수준에 따라 인증 방식이 달라집니다."
|
||||||
|
|
||||||
[msg.dev.clients.help]
|
[msg.dev.clients.help]
|
||||||
@@ -1051,8 +1052,8 @@ name = "Scope Name"
|
|||||||
delete = "Delete"
|
delete = "Delete"
|
||||||
|
|
||||||
[ui.dev.clients.general.security]
|
[ui.dev.clients.general.security]
|
||||||
confidential = "Confidential"
|
private = "Private"
|
||||||
public = "Public"
|
pkce = "PKCE"
|
||||||
title = "보안 설정"
|
title = "보안 설정"
|
||||||
|
|
||||||
[ui.dev.clients.help]
|
[ui.dev.clients.help]
|
||||||
@@ -1085,8 +1086,8 @@ status = "상태"
|
|||||||
type = "유형"
|
type = "유형"
|
||||||
|
|
||||||
[ui.dev.clients.type]
|
[ui.dev.clients.type]
|
||||||
confidential = "기밀(Confidential)"
|
private = "Private"
|
||||||
public = "Public"
|
pkce = "PKCE"
|
||||||
|
|
||||||
[ui.dev.dashboard]
|
[ui.dev.dashboard]
|
||||||
ready_badge = "devfront ready"
|
ready_badge = "devfront ready"
|
||||||
|
|||||||
@@ -248,6 +248,7 @@ note = ""
|
|||||||
load_error = ""
|
load_error = ""
|
||||||
loading = ""
|
loading = ""
|
||||||
saved = ""
|
saved = ""
|
||||||
|
save_error = ""
|
||||||
|
|
||||||
[msg.dev.clients.general.identity]
|
[msg.dev.clients.general.identity]
|
||||||
logo_help = ""
|
logo_help = ""
|
||||||
@@ -261,8 +262,8 @@ empty = ""
|
|||||||
subtitle = ""
|
subtitle = ""
|
||||||
|
|
||||||
[msg.dev.clients.general.security]
|
[msg.dev.clients.general.security]
|
||||||
confidential_help = ""
|
private_help = ""
|
||||||
public_help = ""
|
pkce_help = ""
|
||||||
subtitle = ""
|
subtitle = ""
|
||||||
|
|
||||||
[msg.dev.clients.help]
|
[msg.dev.clients.help]
|
||||||
@@ -1063,8 +1064,8 @@ name = ""
|
|||||||
delete = ""
|
delete = ""
|
||||||
|
|
||||||
[ui.dev.clients.general.security]
|
[ui.dev.clients.general.security]
|
||||||
confidential = ""
|
private = ""
|
||||||
public = ""
|
pkce = ""
|
||||||
title = ""
|
title = ""
|
||||||
|
|
||||||
[ui.dev.clients.help]
|
[ui.dev.clients.help]
|
||||||
@@ -1097,8 +1098,8 @@ status = ""
|
|||||||
type = ""
|
type = ""
|
||||||
|
|
||||||
[ui.dev.clients.type]
|
[ui.dev.clients.type]
|
||||||
confidential = ""
|
private = ""
|
||||||
public = ""
|
pkce = ""
|
||||||
|
|
||||||
[ui.dev.dashboard]
|
[ui.dev.dashboard]
|
||||||
ready_badge = ""
|
ready_badge = ""
|
||||||
|
|||||||
Reference in New Issue
Block a user