1
0
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:
2026-02-23 17:45:08 +09:00
14 changed files with 591 additions and 286 deletions

View File

@@ -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)

View File

@@ -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,
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]
} }
func NewDevHandler(redis domain.RedisRepository, secretRepo domain.ClientSecretRepository, consentRepo repository.ClientConsentRepository, rpSvc service.RelyingPartyService) *DevHandler {
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,

View File

@@ -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))

View File

@@ -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",

View File

@@ -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>

View File

@@ -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>

View File

@@ -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;

View File

@@ -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"

View File

@@ -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"

View File

@@ -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 = ""

View File

@@ -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"],

View File

@@ -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"

View File

@@ -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"

View File

@@ -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 = ""