diff --git a/Makefile b/Makefile index 99d92498..bb5da047 100644 --- a/Makefile +++ b/Makefile @@ -340,7 +340,11 @@ code-check-userfront-lint: code-check-front-lint: @echo "==> adminfront biome lint/format check" rm -rf adminfront/playwright-report adminfront/test-results - cd adminfront && CI=true npx pnpm install --frozen-lockfile --ignore-scripts + @if [ -d adminfront/node_modules ]; then \ + echo "adminfront/node_modules already present; skipping pnpm install."; \ + else \ + cd adminfront && CI=true npx pnpm install --frozen-lockfile --ignore-scripts; \ + fi cd adminfront && npx biome lint . cd adminfront && npx biome format . @echo "==> devfront biome lint/format check" @@ -354,9 +358,13 @@ code-check-front-lint: cd devfront && npx biome format . @echo "==> orgfront biome lint/format check" rm -rf orgfront/playwright-report orgfront/test-results - cd orgfront && npm ci --ignore-scripts - cd orgfront && npx biome lint . - cd orgfront && npx biome format . + @if [ -d orgfront/node_modules ]; then \ + echo "orgfront/node_modules already present; skipping npm install."; \ + else \ + cd orgfront && npm ci --ignore-scripts; \ + fi + cd orgfront && ./node_modules/@biomejs/biome/bin/biome lint . + cd orgfront && ./node_modules/@biomejs/biome/bin/biome format . code-check-backend-tests: @echo "==> backend tests" diff --git a/backend/internal/bootstrap/tenant_seed_test.go b/backend/internal/bootstrap/tenant_seed_test.go index e413498e..e6adb34d 100644 --- a/backend/internal/bootstrap/tenant_seed_test.go +++ b/backend/internal/bootstrap/tenant_seed_test.go @@ -405,5 +405,4 @@ func TestSeedTenantsCreatesMissingSeedRowsAndRepairsExistingSeedSlug(t *testing. if rootCount != 1 { t.Fatalf("existing-root row count = %d, want 1", rootCount) } - } diff --git a/backend/internal/handler/auth_handler_login_test.go b/backend/internal/handler/auth_handler_login_test.go index c4565403..bb335144 100644 --- a/backend/internal/handler/auth_handler_login_test.go +++ b/backend/internal/handler/auth_handler_login_test.go @@ -486,8 +486,8 @@ func runHeadlessPasswordLoginWithAssertionRequest( }) h := &AuthHandler{ - IdpProvider: mockIdp, - KratosAdmin: mockKratos, + IdpProvider: mockIdp, + KratosAdmin: mockKratos, HeadlessJWKS: service.NewHeadlessJWKSCacheService(nil, jwksClient), Hydra: &service.HydraAdminService{ AdminURL: "http://hydra.test", @@ -585,8 +585,8 @@ func runHeadlessPasswordLoginWithAssertionAndLoggerRequest( }) h := &AuthHandler{ - IdpProvider: mockIdp, - KratosAdmin: mockKratos, + IdpProvider: mockIdp, + KratosAdmin: mockKratos, HeadlessJWKS: service.NewHeadlessJWKSCacheService(nil, jwksClient), Hydra: &service.HydraAdminService{ AdminURL: "http://hydra.test", @@ -904,8 +904,8 @@ func TestHeadlessPasswordLogin_HeadlessLoginClientSuccess(t *testing.T) { mockKratos.On("FindIdentityIDByIdentifier", mock.Anything, "employee001").Return("kratos-identity-id", nil) h := &AuthHandler{ - IdpProvider: mockIdp, - KratosAdmin: mockKratos, + IdpProvider: mockIdp, + KratosAdmin: mockKratos, HeadlessJWKS: service.NewHeadlessJWKSCacheService(nil, jwksClient), Hydra: &service.HydraAdminService{ AdminURL: "http://hydra.test", @@ -1002,8 +1002,8 @@ func TestHeadlessPasswordLogin_OIDCSubjectConflictBlocksMixedRP(t *testing.T) { mockKratos.On("FindIdentityIDByIdentifier", mock.Anything, "employee002").Return("kratos-target-b", nil) h := &AuthHandler{ - IdpProvider: mockIdp, - KratosAdmin: mockKratos, + IdpProvider: mockIdp, + KratosAdmin: mockKratos, HeadlessJWKS: service.NewHeadlessJWKSCacheService(nil, jwksClient), Hydra: &service.HydraAdminService{ AdminURL: "http://hydra.test", @@ -1080,8 +1080,8 @@ func TestHeadlessPasswordLogin_OIDCSubjectSameAllowsMixedRP(t *testing.T) { mockKratos.On("FindIdentityIDByIdentifier", mock.Anything, "employee001").Return("kratos-userfront-a", nil) h := &AuthHandler{ - IdpProvider: mockIdp, - KratosAdmin: mockKratos, + IdpProvider: mockIdp, + KratosAdmin: mockKratos, HeadlessJWKS: service.NewHeadlessJWKSCacheService(nil, jwksClient), Hydra: &service.HydraAdminService{ AdminURL: "http://hydra.test", @@ -1281,8 +1281,8 @@ func TestHeadlessPasswordLogin_IgnoresInlineHeadlessJWKSWhenJWKSURIIsConfigured( mockKratos.On("FindIdentityIDByIdentifier", mock.Anything, "employee001").Return("kratos-identity-id", nil) h := &AuthHandler{ - IdpProvider: mockIdp, - KratosAdmin: mockKratos, + IdpProvider: mockIdp, + KratosAdmin: mockKratos, HeadlessJWKS: service.NewHeadlessJWKSCacheService(nil, jwksClient), Hydra: &service.HydraAdminService{ AdminURL: "http://hydra.test", @@ -1544,8 +1544,8 @@ func TestHeadlessPasswordLogin_InvalidClientAssertionRejected(t *testing.T) { }) h := &AuthHandler{ - IdpProvider: mockIdp, - KratosAdmin: mockKratos, + IdpProvider: mockIdp, + KratosAdmin: mockKratos, HeadlessJWKS: service.NewHeadlessJWKSCacheService(nil, jwksClient), Hydra: &service.HydraAdminService{ AdminURL: "http://hydra.test", diff --git a/backend/internal/handler/dev_handler.go b/backend/internal/handler/dev_handler.go index 1863ac78..014031f5 100644 --- a/backend/internal/handler/dev_handler.go +++ b/backend/internal/handler/dev_handler.go @@ -293,7 +293,7 @@ func normalizeDeveloperAccessPagesForHandler(pages []string) []string { } if page == domain.DeveloperAccessPageAll { normalized = []string{domain.DeveloperAccessPageAll} - seen = map[string]struct{}{domain.DeveloperAccessPageAll: struct{}{}} + seen = map[string]struct{}{domain.DeveloperAccessPageAll: {}} return } for _, allowed := range domain.DeveloperAccessPageOrder { diff --git a/backend/internal/service/developer_service.go b/backend/internal/service/developer_service.go index 799a1e2e..566b667b 100644 --- a/backend/internal/service/developer_service.go +++ b/backend/internal/service/developer_service.go @@ -28,7 +28,7 @@ func normalizeDeveloperAccessPages(pages []string) []string { } if page == domain.DeveloperAccessPageAll { normalized = []string{domain.DeveloperAccessPageAll} - seen = map[string]struct{}{domain.DeveloperAccessPageAll: struct{}{}} + seen = map[string]struct{}{domain.DeveloperAccessPageAll: {}} return } if page != domain.DeveloperAccessPageOverview && diff --git a/devfront/src/features/clients/ClientConsentsPage.test.tsx b/devfront/src/features/clients/ClientConsentsPage.test.tsx new file mode 100644 index 00000000..c8d9cede --- /dev/null +++ b/devfront/src/features/clients/ClientConsentsPage.test.tsx @@ -0,0 +1,204 @@ +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import { act } from "react"; +import { createRoot, type Root } from "react-dom/client"; +import { MemoryRouter, Route, Routes } from "react-router-dom"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import ClientConsentsPage from "./ClientConsentsPage"; + +const fetchClientMock = vi.fn(); +const fetchConsentsMock = vi.fn(); +const fetchRPUserMetadataMock = vi.fn(); +const updateRPUserMetadataMock = vi.fn(); +const revokeConsentMock = vi.fn(); + +vi.mock("../../lib/devApi", () => ({ + fetchClient: (...args: unknown[]) => fetchClientMock(...args), + fetchConsents: (...args: unknown[]) => fetchConsentsMock(...args), + fetchRPUserMetadata: (...args: unknown[]) => fetchRPUserMetadataMock(...args), + updateRPUserMetadata: (...args: unknown[]) => + updateRPUserMetadataMock(...args), + revokeConsent: (...args: unknown[]) => revokeConsentMock(...args), +})); + +vi.mock("../../lib/i18n", () => ({ + t: (key: string, fallback?: string, vars?: Record) => { + let text = fallback ?? key; + for (const [name, value] of Object.entries(vars ?? {})) { + text = text.replaceAll(`{{${name}}}`, String(value)); + } + return text; + }, +})); + +const roots: Root[] = []; + +const clientDetail = { + client: { + id: "client-a", + name: "Claims App", + type: "private" as const, + status: "active" as const, + redirectUris: ["https://rp.example.com/callback"], + scopes: ["openid", "profile"], + tokenEndpointAuthMethod: "client_secret_basic", + metadata: { + id_token_claims: [ + { + namespace: "rp_claims", + key: "license", + value: "12345678", + valueType: "text", + readPermission: "admin_only", + writePermission: "admin_only", + }, + ], + }, + }, + endpoints: { + discovery: "https://issuer/.well-known/openid-configuration", + issuer: "https://issuer", + authorization: "https://issuer/oauth2/auth", + token: "https://issuer/oauth2/token", + userinfo: "https://issuer/userinfo", + }, +}; + +function buildMetadata() { + return { + license: "abcd", + license_permissions: { + readPermission: "user_and_admin", + writePermission: "admin_only", + }, + }; +} + +async function flush() { + await act(async () => { + await new Promise((resolve) => setTimeout(resolve, 0)); + }); +} + +async function renderPage() { + const container = document.createElement("div"); + document.body.appendChild(container); + const root = createRoot(container); + roots.push(root); + const queryClient = new QueryClient({ + defaultOptions: { + queries: { retry: false }, + mutations: { retry: false }, + }, + }); + + await act(async () => { + root.render( + + + + } + /> + + + , + ); + }); + await flush(); + + return { container }; +} + +describe("ClientConsentsPage RP custom claims", () => { + beforeEach(() => { + fetchClientMock.mockResolvedValue(clientDetail); + 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: buildMetadata(), + }, + ], + }); + fetchRPUserMetadataMock.mockResolvedValue({ + clientId: "client-a", + userId: "user-1", + metadata: buildMetadata(), + }); + updateRPUserMetadataMock.mockResolvedValue({ + clientId: "client-a", + userId: "user-1", + metadata: buildMetadata(), + }); + revokeConsentMock.mockResolvedValue(undefined); + }); + + afterEach(() => { + for (const root of roots.splice(0)) { + act(() => { + root.unmount(); + }); + } + vi.clearAllMocks(); + document.body.innerHTML = ""; + }); + + it("removes the RP custom claim permission selectors while keeping claim data editable", async () => { + const { container } = await renderPage(); + + const editButton = Array.from(container.querySelectorAll("button")).find( + (button) => + button.textContent?.includes("사용자 Claim 설정") || + button.textContent?.includes("User Claim Settings"), + ); + expect(editButton).toBeDefined(); + + await act(async () => { + editButton?.dispatchEvent(new MouseEvent("click", { bubbles: true })); + }); + await flush(); + + expect( + container.querySelectorAll('select[aria-label="읽기 권한"]'), + ).toHaveLength(0); + expect( + container.querySelectorAll('select[aria-label="쓰기 권한"]'), + ).toHaveLength(0); + expect(container.textContent).toContain("license"); + expect(container.textContent).toContain("abcd"); + + const saveButton = Array.from(container.querySelectorAll("button")).find( + (button) => + button.textContent?.includes("Claim 저장") || + button.textContent?.includes("Save"), + ); + expect(saveButton).toBeDefined(); + + await act(async () => { + saveButton?.dispatchEvent(new MouseEvent("click", { bubbles: true })); + }); + await flush(); + + expect(updateRPUserMetadataMock).toHaveBeenCalledWith( + "client-a", + "user-1", + expect.objectContaining({ + license: "abcd", + license_permissions: { + readPermission: "user_and_admin", + writePermission: "admin_only", + }, + }), + ); + }); +}); diff --git a/devfront/src/features/clients/ClientConsentsPage.tsx b/devfront/src/features/clients/ClientConsentsPage.tsx index a3dd2824..1e6f1fa9 100644 --- a/devfront/src/features/clients/ClientConsentsPage.tsx +++ b/devfront/src/features/clients/ClientConsentsPage.tsx @@ -1023,7 +1023,7 @@ function ClientConsentsPage() { metadataDraftRows.map((row) => (
{row.key} @@ -1036,7 +1036,7 @@ function ClientConsentsPage() { value: event.target.value, }) } - className="h-10 rounded-md border border-input bg-background px-3 font-mono text-xs shadow-sm focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring" + className="h-10 w-full max-w-[180px] rounded-md border border-input bg-background px-3 font-mono text-xs shadow-sm focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring" aria-label={`${row.key} boolean`} > @@ -1051,12 +1051,12 @@ function ClientConsentsPage() { value: event.target.value, }) } - className="min-h-10 font-mono text-xs" placeholder={ row.valueType === "array" ? `["value"]` : `{"key": "value"}` } + className="min-h-10 w-full max-w-[320px] font-mono text-xs" aria-label={`${row.key} ${row.valueType}`} /> ) : ( @@ -1071,7 +1071,7 @@ function ClientConsentsPage() { value: event.target.value, }) } - className="font-mono text-xs" + className="w-full max-w-[320px] font-mono text-xs" placeholder={t( "ui.dev.clients.consents.rp_claims.value_placeholder", "claim value", @@ -1099,63 +1099,9 @@ function ClientConsentsPage() { )}
)} - - {row.valueType} diff --git a/devfront/src/features/clients/ClientGeneralPage.claims.test.tsx b/devfront/src/features/clients/ClientGeneralPage.claims.test.tsx index 547993c6..a244ee31 100644 --- a/devfront/src/features/clients/ClientGeneralPage.claims.test.tsx +++ b/devfront/src/features/clients/ClientGeneralPage.claims.test.tsx @@ -68,7 +68,36 @@ vi.mock("../../lib/i18n", () => ({ const roots: Root[] = []; -function makeClientDetail(claimKey: string): ClientDetailResponse { +function makeClientDetail( + claimKey: string, + options?: { + includeTenantScope?: boolean; + tenantAccessRestricted?: boolean; + tenantScopeMandatory?: boolean; + }, +): ClientDetailResponse { + const includeTenantScope = options?.includeTenantScope ?? false; + const tenantAccessRestricted = options?.tenantAccessRestricted ?? false; + const tenantScopeMandatory = options?.tenantScopeMandatory ?? false; + const structuredScopes = [ + { + id: "1", + name: "openid", + description: "", + mandatory: true, + }, + ]; + + if (includeTenantScope) { + structuredScopes.push({ + id: "2", + name: "tenant", + description: "Tenant access", + mandatory: tenantScopeMandatory, + locked: tenantAccessRestricted, + }); + } + return { client: { id: "client-claims", @@ -76,18 +105,14 @@ function makeClientDetail(claimKey: string): ClientDetailResponse { type: "private", status: "active", redirectUris: ["https://rp.example.com/callback"], - scopes: ["openid", "profile"], + scopes: includeTenantScope + ? ["openid", "tenant", "profile"] + : ["openid", "profile"], tokenEndpointAuthMethod: "client_secret_basic", metadata: { description: "Claims app", - structured_scopes: [ - { - id: "1", - name: "openid", - description: "", - mandatory: true, - }, - ], + tenant_access_restricted: tenantAccessRestricted, + structured_scopes: structuredScopes, id_token_claims: [ { namespace: "rp_claims", @@ -304,6 +329,75 @@ describe("ClientGeneralPage RP claims", () => { ); }); + it("preserves tenant scope mandatory state when tenant access restriction is off", async () => { + fetchClientMock.mockResolvedValue( + makeClientDetail("old_claim", { + includeTenantScope: true, + tenantAccessRestricted: false, + tenantScopeMandatory: true, + }), + ); + updateClientMock.mockResolvedValue( + makeClientDetail("old_claim", { + includeTenantScope: true, + tenantAccessRestricted: false, + tenantScopeMandatory: false, + }), + ); + + const { container } = await renderPage(); + + const tenantScopeRow = Array.from(container.querySelectorAll("tr")).find( + (row) => + Array.from(row.querySelectorAll("input")).some( + (input) => (input as HTMLInputElement).value === "tenant", + ), + ); + + expect(tenantScopeRow).toBeDefined(); + const mandatorySwitch = + tenantScopeRow?.querySelector('[role="switch"]'); + expect(mandatorySwitch).toBeDefined(); + expect(mandatorySwitch?.getAttribute("aria-checked")).toBe("true"); + + await act(async () => { + mandatorySwitch?.dispatchEvent( + new MouseEvent("click", { bubbles: true }), + ); + }); + await flush(); + + expect(mandatorySwitch?.getAttribute("aria-checked")).toBe("false"); + + const saveButton = Array.from(container.querySelectorAll("button")).find( + (button) => + button.textContent?.includes("저장") || + button.textContent?.includes("Save"), + ); + expect(saveButton).toBeDefined(); + + await act(async () => { + saveButton?.dispatchEvent(new MouseEvent("click", { bubbles: true })); + }); + await flush(); + + expect(updateClientMock).toHaveBeenCalledWith( + "client-claims", + expect.objectContaining({ + metadata: expect.objectContaining({ + tenant_access_restricted: false, + structured_scopes: expect.arrayContaining([ + expect.objectContaining({ + name: "tenant", + mandatory: false, + locked: false, + }), + ]), + }), + }), + ); + }); + it("keeps nullable and default value as separate RP claim settings", async () => { const { container } = await renderPage(); diff --git a/devfront/src/features/clients/ClientGeneralPage.tsx b/devfront/src/features/clients/ClientGeneralPage.tsx index 98f320c6..af826426 100644 --- a/devfront/src/features/clients/ClientGeneralPage.tsx +++ b/devfront/src/features/clients/ClientGeneralPage.tsx @@ -689,11 +689,18 @@ function ClientGeneralPage() { if (scope.name.trim() !== "tenant") { return scope; } + if (restricted) { + return { + ...scope, + description: scope.description || tenantScopeDescription, + mandatory: true, + locked: true, + }; + } return { ...scope, description: scope.description || tenantScopeDescription, - mandatory: restricted, - locked: restricted, + locked: false, }; }); diff --git a/devfront/tests/devfront-client-claims-cache.spec.ts b/devfront/tests/devfront-client-claims-cache.spec.ts index c822c651..4a21e3c3 100644 --- a/devfront/tests/devfront-client-claims-cache.spec.ts +++ b/devfront/tests/devfront-client-claims-cache.spec.ts @@ -354,10 +354,12 @@ test.describe("DevFront RP claim cache", () => { await valueTypeSelect.selectOption("number"); await expect(valueTypeSelect).toHaveValue("number"); - await page + const defaultValueInput = page .getByPlaceholder(/기본값을 입력하세요|Enter the default value/i) - .first() - .fill("3.14"); + .first(); + await expect(defaultValueInput).toHaveAttribute("inputmode", "numeric"); + await expect(defaultValueInput).toHaveAttribute("pattern", "-?[0-9]*"); + await defaultValueInput.fill("3.14"); await expect( page.getByText(/Claim 기본값이 타입과 맞지 않습니다|does not match/i), diff --git a/devfront/tests/devfront-clients-lifecycle.spec.ts b/devfront/tests/devfront-clients-lifecycle.spec.ts index 16e4c0cc..578ca9f1 100644 --- a/devfront/tests/devfront-clients-lifecycle.spec.ts +++ b/devfront/tests/devfront-clients-lifecycle.spec.ts @@ -167,7 +167,9 @@ test.describe("DevFront clients lifecycle", () => { .first() .click(); await page - .getByLabel(/Read 사용자 허용|Read user allowed/i) + .getByLabel( + /Read 사용자 허용|Read user allowed|사용자 읽기 허용|Allow user read/i, + ) .first() .click(); @@ -347,10 +349,18 @@ test.describe("DevFront clients lifecycle", () => { ).toHaveValue("2"); await expect(page.getByLabel(/Nullable|Null 허용/i).first()).toBeChecked(); await expect( - page.getByLabel(/Read 사용자 허용|Read user allowed/i).first(), + page + .getByLabel( + /Read 사용자 허용|Read user allowed|사용자 읽기 허용|Allow user read/i, + ) + .first(), ).toBeChecked(); await expect( - page.getByLabel(/Write 사용자 허용|Write user allowed/i).first(), + page + .getByLabel( + /Write 사용자 허용|Write user allowed|사용자 쓰기 허용|Allow user write/i, + ) + .first(), ).not.toBeChecked(); }); diff --git a/devfront/tests/devfront-consents.spec.ts b/devfront/tests/devfront-consents.spec.ts index 07ad54f6..c5e415fa 100644 --- a/devfront/tests/devfront-consents.spec.ts +++ b/devfront/tests/devfront-consents.spec.ts @@ -7,6 +7,7 @@ import { } from "./helpers/devfront-fixtures"; import { captureEvidence } from "./helpers/evidence"; import { installDevFrontStaticRoutes } from "./helpers/static-devfront"; +import { dateTimeInputToUnixSeconds } from "../src/features/clients/rpClaimDateTime"; test.describe("DevFront consents", () => { test.afterEach(async ({ page }, testInfo) => { @@ -107,17 +108,22 @@ test.describe("DevFront consents", () => { .getByLabel(/active_member.*boolean|boolean.*active_member/i) .selectOption("false"); await page.getByLabel(/score.*number|number.*score/i).fill("42"); - await page - .getByLabel(/쓰기 권한|Write permission/i) - .first() - .selectOption("user_and_admin"); await page.getByRole("button", { name: /Claim 저장|Save Claim/i }).click(); + const browserTimeZone = await page.evaluate( + () => Intl.DateTimeFormat().resolvedOptions().timeZone, + ); await expect .poll(() => state.consents[0]?.rpMetadata?.contract_date) - .toBe(1781017200); + .toBe(dateTimeInputToUnixSeconds("2026-06-10", "date", browserTimeZone)); await expect .poll(() => state.consents[0]?.rpMetadata?.approved_at) - .toBe(1780968600); + .toBe( + dateTimeInputToUnixSeconds( + "2026-06-09T10:30", + "datetime", + browserTimeZone, + ), + ); await expect .poll(() => state.consents[0]?.rpMetadata?.active_member) .toBe(false); @@ -131,7 +137,7 @@ test.describe("DevFront consents", () => { | undefined )?.writePermission, ) - .toBe("user_and_admin"); + .toBe("admin_only"); await page.getByRole("button", { name: /권한 철회|철회|Revoke/i }).click(); await expect(page.getByText(/Revoked|철회/i).first()).toBeVisible(); diff --git a/scripts/run_adminfront_ci_tests.sh b/scripts/run_adminfront_ci_tests.sh index 986eaadd..eb9d33af 100755 --- a/scripts/run_adminfront_ci_tests.sh +++ b/scripts/run_adminfront_ci_tests.sh @@ -298,6 +298,7 @@ echo "==> adminfront using PORT=$port" ( cd "$tmp_dir/adminfront" CI=true PORT="$port" PLAYWRIGHT_WORKERS="${PLAYWRIGHT_WORKERS:-1}" \ + ADMINFRONT_DIST_DIR="$tmp_dir/adminfront/dist" \ pnpm exec playwright test --max-failures=1 "${playwright_project_args[@]}" ) 2>&1 | tee reports/adminfront-test.log test_exit_code=${PIPESTATUS[0]} diff --git a/userfront/assets/translations/en.toml b/userfront/assets/translations/en.toml index fcc1f86e..3e53d2de 100644 --- a/userfront/assets/translations/en.toml +++ b/userfront/assets/translations/en.toml @@ -72,6 +72,7 @@ error = "An error occurred while cancelling consent: {error}" [msg.userfront.consent.scope] email = "Email address (account identification and notifications)" +offline_access = "Offline access (keep signed in)" openid = "OpenID authentication information (signin session check)" phone = "Phone number (identity verification and notifications)" profile = "Basic profile information (name, user identifier)" @@ -705,3 +706,4 @@ toggle_label = "Show active sessions only" [msg.userfront.audit.filter] description = "Toggle to view only active sessions." + diff --git a/userfront/assets/translations/ko.toml b/userfront/assets/translations/ko.toml index 725b4862..bd48e9d8 100644 --- a/userfront/assets/translations/ko.toml +++ b/userfront/assets/translations/ko.toml @@ -297,6 +297,7 @@ error = "취소 처리 중 오류가 발생했습니다: {error}" [msg.userfront.consent.scope] email = "이메일 주소 (계정 식별 및 알림 용도)" +offline_access = "오프라인 접근 (로그인 유지)" openid = "OpenID 인증 정보 (로그인 상태 확인)" phone = "휴대폰 번호 (본인 인증 및 알림)" profile = "기본 프로필 정보 (이름, 사용자 식별자)" @@ -926,3 +927,4 @@ toggle_label = "활성 세션만 보기" [msg.userfront.audit.filter] description = "활성화된 세션만 보려면 토글을 켜주세요." + diff --git a/userfront/assets/translations/template.toml b/userfront/assets/translations/template.toml index 2ddc2d11..09b991d2 100644 --- a/userfront/assets/translations/template.toml +++ b/userfront/assets/translations/template.toml @@ -269,6 +269,7 @@ error = "" [msg.userfront.consent.scope] email = "" +offline_access = "" openid = "" phone = "" profile = "" @@ -898,3 +899,4 @@ toggle_label = "" [msg.userfront.audit.filter] description = "" + diff --git a/userfront/pubspec.lock b/userfront/pubspec.lock index b23d80a9..8b6fff8c 100644 --- a/userfront/pubspec.lock +++ b/userfront/pubspec.lock @@ -45,10 +45,10 @@ packages: dependency: transitive description: name: characters - sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b + sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 url: "https://pub.dev" source: hosted - version: "1.4.1" + version: "1.4.0" cli_config: dependency: transitive description: @@ -268,6 +268,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.5" + js: + dependency: transitive + description: + name: js + sha256: "53385261521cc4a0c4658fd0ad07a7d14591cf8fc33abbceae306ddb974888dc" + url: "https://pub.dev" + source: hosted + version: "0.7.2" leak_tracker: dependency: transitive description: @@ -320,18 +328,18 @@ packages: dependency: transitive description: name: matcher - sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861 + sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 url: "https://pub.dev" source: hosted - version: "0.12.19" + version: "0.12.17" material_color_utilities: dependency: transitive description: name: material_color_utilities - sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b" + sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec url: "https://pub.dev" source: hosted - version: "0.13.0" + version: "0.11.1" meta: dependency: transitive description: @@ -653,26 +661,26 @@ packages: dependency: transitive description: name: test - sha256: "280d6d890011ca966ad08df7e8a4ddfab0fb3aa49f96ed6de56e3521347a9ae7" + sha256: "75906bf273541b676716d1ca7627a17e4c4070a3a16272b7a3dc7da3b9f3f6b7" url: "https://pub.dev" source: hosted - version: "1.30.0" + version: "1.26.3" test_api: dependency: transitive description: name: test_api - sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a" + sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55 url: "https://pub.dev" source: hosted - version: "0.7.10" + version: "0.7.7" test_core: dependency: transitive description: name: test_core - sha256: "0381bd1585d1a924763c308100f2138205252fb90c9d4eeaf28489ee65ccde51" + sha256: "0cc24b5ff94b38d2ae73e1eb43cc302b77964fbf67abad1e296025b78deb53d0" url: "https://pub.dev" source: hosted - version: "0.6.16" + version: "0.6.12" toml: dependency: "direct main" description: