From fb7a05797c98aa055319f4423c19a90e9932d327 Mon Sep 17 00:00:00 2001 From: kyy Date: Fri, 12 Jun 2026 09:01:08 +0900 Subject: [PATCH 1/3] =?UTF-8?q?date/timezone=20=ED=95=9C=20=EC=A4=84=20?= =?UTF-8?q?=EC=A0=95=EB=A0=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../clients/ClientConsentsPage.test.tsx | 81 +++++++++++++++++++ .../features/clients/ClientConsentsPage.tsx | 12 ++- 2 files changed, 91 insertions(+), 2 deletions(-) diff --git a/devfront/src/features/clients/ClientConsentsPage.test.tsx b/devfront/src/features/clients/ClientConsentsPage.test.tsx index c8d9cede..7a76d587 100644 --- a/devfront/src/features/clients/ClientConsentsPage.test.tsx +++ b/devfront/src/features/clients/ClientConsentsPage.test.tsx @@ -201,4 +201,85 @@ describe("ClientConsentsPage RP custom claims", () => { }), ); }); + + it("keeps date claim inputs and timezone selectors on the same row", async () => { + fetchClientMock.mockResolvedValue({ + ...clientDetail, + client: { + ...clientDetail.client, + metadata: { + id_token_claims: [ + { + namespace: "rp_claims", + key: "contract_date", + value: "", + valueType: "date", + readPermission: "admin_only", + writePermission: "admin_only", + }, + ], + }, + }, + }); + fetchConsentsMock.mockResolvedValue({ + items: [ + { + subject: "user-1", + userName: "Consent User", + clientId: "client-a", + clientName: "Claims App", + grantedScopes: ["openid", "profile"], + authenticatedAt: "2026-06-11T09:00:00Z", + createdAt: "2026-06-10T09:00:00Z", + status: "active", + tenantId: "tenant-1", + tenantName: "Hanmac", + rpMetadata: { + contract_date: 1781017200, + contract_date_permissions: { + readPermission: "admin_only", + writePermission: "admin_only", + }, + }, + }, + ], + }); + fetchRPUserMetadataMock.mockResolvedValue({ + clientId: "client-a", + userId: "user-1", + metadata: { + contract_date: 1781017200, + contract_date_permissions: { + readPermission: "admin_only", + writePermission: "admin_only", + }, + }, + }); + + const { container } = await renderPage(); + + const editButton = Array.from(container.querySelectorAll("button")).find( + (button) => + button.textContent?.includes("사용자 Claim 설정") || + button.textContent?.includes("User Claim Settings"), + ); + + await act(async () => { + editButton?.dispatchEvent(new MouseEvent("click", { bubbles: true })); + }); + await flush(); + + const dateInput = container.querySelector( + 'input[aria-label="contract_date date"]', + ); + const timeZoneSelect = container.querySelector( + 'select[aria-label="contract_date timezone"]', + ); + + expect(dateInput).not.toBeNull(); + expect(timeZoneSelect).not.toBeNull(); + expect(dateInput?.parentElement).toBe(timeZoneSelect?.parentElement); + expect(dateInput?.parentElement?.className).toContain("items-center"); + expect(dateInput?.parentElement?.className).not.toContain("flex-col"); + }); }); diff --git a/devfront/src/features/clients/ClientConsentsPage.tsx b/devfront/src/features/clients/ClientConsentsPage.tsx index 1e6f1fa9..b76e0175 100644 --- a/devfront/src/features/clients/ClientConsentsPage.tsx +++ b/devfront/src/features/clients/ClientConsentsPage.tsx @@ -1060,7 +1060,15 @@ function ClientConsentsPage() { aria-label={`${row.key} ${row.valueType}`} /> ) : ( -
+
{timeZoneOptions.map((timeZone) => ( From ca15e2a35c714e074035a6b83e8611762ce83fcb Mon Sep 17 00:00:00 2001 From: kyy Date: Fri, 12 Jun 2026 14:54:49 +0900 Subject: [PATCH 2/3] =?UTF-8?q?offline=5Faccess=20=EA=B8=B0=EB=B3=B8=20?= =?UTF-8?q?=EC=8A=A4=EC=BD=94=ED=94=84=20=EC=B6=94=EA=B0=80=20=EB=B0=8F=20?= =?UTF-8?q?refresh=5Ftoken=20=EB=B0=9C=EA=B8=89=20=ED=99=95=EC=9D=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/internal/handler/auth_handler.go | 2 +- .../internal/handler/client_tenant_access.go | 4 ++-- .../handler/client_tenant_access_test.go | 10 ++++++---- backend/internal/handler/dev_handler.go | 8 ++++---- backend/internal/handler/dev_handler_test.go | 8 ++++---- .../clients/ClientGeneralPage.claims.test.tsx | 4 ++-- .../src/features/clients/ClientGeneralPage.tsx | 18 ++++++++++++++++++ .../tests/devfront-client-claims-cache.spec.ts | 8 ++++---- 8 files changed, 41 insertions(+), 21 deletions(-) diff --git a/backend/internal/handler/auth_handler.go b/backend/internal/handler/auth_handler.go index e92041d3..185a6c68 100644 --- a/backend/internal/handler/auth_handler.go +++ b/backend/internal/handler/auth_handler.go @@ -8430,7 +8430,7 @@ func buildHydraAuthorizationURL(clientID string, scopes []string, redirectURIs [ seen := map[string]struct{}{} for _, scope := range append([]string{"openid"}, scopes...) { scope = strings.TrimSpace(scope) - if scope == "" || isRefreshTokenScopeAlias(scope) { + if scope == "" || isLegacyRefreshTokenScopeAlias(scope) { continue } if _, ok := seen[scope]; ok { diff --git a/backend/internal/handler/client_tenant_access.go b/backend/internal/handler/client_tenant_access.go index 670980b4..a5f9bbe3 100644 --- a/backend/internal/handler/client_tenant_access.go +++ b/backend/internal/handler/client_tenant_access.go @@ -464,7 +464,7 @@ func normalizeScopesInConsentOrder(scopes []string) []string { appendIfPresent := func(scope string) { scope = strings.TrimSpace(scope) - if scope == "" || isRefreshTokenScopeAlias(scope) { + if scope == "" || isLegacyRefreshTokenScopeAlias(scope) { return } if _, ok := seen[scope]; ok { @@ -485,7 +485,7 @@ func normalizeScopesInConsentOrder(scopes []string) []string { for _, scope := range combined { scope = strings.TrimSpace(scope) - if scope == "" || isRefreshTokenScopeAlias(scope) { + if scope == "" || isLegacyRefreshTokenScopeAlias(scope) { continue } if _, ok := seen[scope]; ok { diff --git a/backend/internal/handler/client_tenant_access_test.go b/backend/internal/handler/client_tenant_access_test.go index 29caeeb9..661cd631 100644 --- a/backend/internal/handler/client_tenant_access_test.go +++ b/backend/internal/handler/client_tenant_access_test.go @@ -9,6 +9,7 @@ import ( "net/http" "net/http/httptest" "net/url" + "strings" "testing" "github.com/gofiber/fiber/v2" @@ -153,7 +154,7 @@ func TestMergeRequestedScopesWithClientRequirements_StripsRefreshTokenScopeAlias []string{"openid", "offline", "profile", "offline_access"}, ) - assert.Equal(t, []string{"openid", "tenant", "profile", "email"}, merged) + assert.Equal(t, []string{"openid", "tenant", "profile", "offline_access", "email"}, merged) } func TestBuildHydraAuthorizationURL_StripsRefreshTokenScopeAliases(t *testing.T) { @@ -166,10 +167,11 @@ func TestBuildHydraAuthorizationURL_StripsRefreshTokenScopeAliases(t *testing.T) parsed, err := url.Parse(urlString) assert.NoError(t, err) scopes := parsed.Query().Get("scope") + scopeItems := strings.Fields(scopes) - assert.Equal(t, "openid profile email", scopes) - assert.NotContains(t, scopes, "offline") - assert.NotContains(t, scopes, "offline_access") + assert.Equal(t, "openid profile offline_access email", scopes) + assert.NotContains(t, scopeItems, "offline") + assert.Contains(t, scopeItems, "offline_access") } func TestGetConsentRequest_DeniesTenantAccess(t *testing.T) { diff --git a/backend/internal/handler/dev_handler.go b/backend/internal/handler/dev_handler.go index 014031f5..eacfdd83 100644 --- a/backend/internal/handler/dev_handler.go +++ b/backend/internal/handler/dev_handler.go @@ -3828,7 +3828,7 @@ func requestIncludesInlineHeadlessJWKS(req clientUpsertRequest) bool { } func defaultClientScopes() []string { - return []string{"openid", "profile", "email"} + return []string{"openid", "profile", "email", "offline_access"} } func defaultGrantTypes() []string { @@ -3848,7 +3848,7 @@ func normalizeClientScopes(scopes []string) []string { seen := make(map[string]struct{}, len(scopes)) for _, scope := range scopes { scope = strings.TrimSpace(scope) - if scope == "" || isRefreshTokenScopeAlias(scope) { + if scope == "" || isLegacyRefreshTokenScopeAlias(scope) { continue } if _, ok := seen[scope]; ok { @@ -3860,9 +3860,9 @@ func normalizeClientScopes(scopes []string) []string { return normalized } -func isRefreshTokenScopeAlias(scope string) bool { +func isLegacyRefreshTokenScopeAlias(scope string) bool { switch strings.ToLower(strings.TrimSpace(scope)) { - case "offline", "offline_access": + case "offline": return true default: return false diff --git a/backend/internal/handler/dev_handler_test.go b/backend/internal/handler/dev_handler_test.go index d750e9fd..0ababf7b 100644 --- a/backend/internal/handler/dev_handler_test.go +++ b/backend/internal/handler/dev_handler_test.go @@ -2229,9 +2229,9 @@ func TestCreateClient_StripsOfflineScopesAndKeepsRefreshTokenGrant(t *testing.T) resp, _ := app.Test(req, -1) assert.Equal(t, http.StatusCreated, resp.StatusCode) - assert.Equal(t, "openid profile email", captured.Scope) + assert.Equal(t, "openid profile offline_access email", captured.Scope) assert.NotContains(t, strings.Fields(captured.Scope), "offline") - assert.NotContains(t, strings.Fields(captured.Scope), "offline_access") + assert.Contains(t, strings.Fields(captured.Scope), "offline_access") assert.Contains(t, captured.GrantTypes, "refresh_token") } @@ -2296,9 +2296,9 @@ func TestUpdateClient_StripsStoredOfflineScopesAndKeepsRefreshTokenGrant(t *test resp, _ := app.Test(req, -1) assert.Equal(t, http.StatusOK, resp.StatusCode) - assert.Equal(t, "openid profile email", captured.Scope) + assert.Equal(t, "openid profile offline_access email", captured.Scope) assert.NotContains(t, strings.Fields(captured.Scope), "offline") - assert.NotContains(t, strings.Fields(captured.Scope), "offline_access") + assert.Contains(t, strings.Fields(captured.Scope), "offline_access") assert.Contains(t, captured.GrantTypes, "refresh_token") } diff --git a/devfront/src/features/clients/ClientGeneralPage.claims.test.tsx b/devfront/src/features/clients/ClientGeneralPage.claims.test.tsx index a244ee31..2aa94e53 100644 --- a/devfront/src/features/clients/ClientGeneralPage.claims.test.tsx +++ b/devfront/src/features/clients/ClientGeneralPage.claims.test.tsx @@ -409,7 +409,7 @@ describe("ClientGeneralPage RP claims", () => { ); }); - it("shows supported scopes and custom claims without integrated offline_access from the add scope button", async () => { + it("shows supported scopes including offline_access and custom claims from the add scope button", async () => { const { container } = await renderPage(); const addScopeButton = Array.from( @@ -422,7 +422,7 @@ describe("ClientGeneralPage RP claims", () => { }); await flush(); - expect(container.textContent).not.toContain("offline_access"); + expect(container.textContent).toContain("offline_access"); expect(container.textContent).toContain("old_claim"); const customClaimButton = Array.from( diff --git a/devfront/src/features/clients/ClientGeneralPage.tsx b/devfront/src/features/clients/ClientGeneralPage.tsx index af826426..710c7e5b 100644 --- a/devfront/src/features/clients/ClientGeneralPage.tsx +++ b/devfront/src/features/clients/ClientGeneralPage.tsx @@ -659,6 +659,15 @@ function ClientGeneralPage() { description: t("msg.dev.clients.scopes.email", "이메일 주소 접근"), mandatory: false, }, + { + id: "5", + name: "offline_access", + description: t( + "msg.dev.clients.scopes.offline_access", + "refresh token 발급 요청", + ), + mandatory: false, + }, ]); const [idTokenClaims, setIdTokenClaims] = useState([]); const browserTimeZone = useMemo(() => getBrowserTimeZone(), []); @@ -759,6 +768,15 @@ function ClientGeneralPage() { description: tenantScopeDescription, source: "standard", }, + { + id: "standard-offline-access", + name: "offline_access", + description: t( + "msg.dev.clients.scopes.offline_access", + "refresh token 발급 요청", + ), + source: "standard", + }, ], [tenantScopeDescription], ); diff --git a/devfront/tests/devfront-client-claims-cache.spec.ts b/devfront/tests/devfront-client-claims-cache.spec.ts index 4a21e3c3..cf5161a7 100644 --- a/devfront/tests/devfront-client-claims-cache.spec.ts +++ b/devfront/tests/devfront-client-claims-cache.spec.ts @@ -99,7 +99,7 @@ test.describe("DevFront RP claim cache", () => { await expect(claimKeyInput).toHaveValue("new_claim"); }); - test("adds supported scopes and custom claim keys from the scope picker without offline_access", async ({ + test("adds supported scopes and custom claim keys from the scope picker including offline_access", async ({ page, }) => { const state = { @@ -142,9 +142,9 @@ test.describe("DevFront RP claim cache", () => { .getByRole("button", { name: /스코프 추가|Scope 추가|Add Scope/i }) .click(); - await expect(page.getByText("offline_access", { exact: true })).toHaveCount( - 0, - ); + await expect( + page.getByText("offline_access", { exact: true }), + ).toBeVisible(); await expect( page.getByRole("button", { name: /employee_code/ }), ).toBeVisible(); From c587f370899c928c166f48c003c07646a47ae687 Mon Sep 17 00:00:00 2001 From: kyy Date: Fri, 12 Jun 2026 15:02:45 +0900 Subject: [PATCH 3/3] =?UTF-8?q?ClientConsentsPage=20Biome=20=ED=8F=AC?= =?UTF-8?q?=EB=A7=B7=20=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- devfront/src/features/clients/ClientConsentsPage.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/devfront/src/features/clients/ClientConsentsPage.tsx b/devfront/src/features/clients/ClientConsentsPage.tsx index b76e0175..9ccdfcf8 100644 --- a/devfront/src/features/clients/ClientConsentsPage.tsx +++ b/devfront/src/features/clients/ClientConsentsPage.tsx @@ -1063,8 +1063,7 @@ function ClientConsentsPage() {