diff --git a/adminfront/src/features/tenants/routes/TenantListPage.tsx b/adminfront/src/features/tenants/routes/TenantListPage.tsx index 94d32e33..d4e3bebb 100644 --- a/adminfront/src/features/tenants/routes/TenantListPage.tsx +++ b/adminfront/src/features/tenants/routes/TenantListPage.tsx @@ -755,7 +755,9 @@ function TenantListPage() { {t(`ui.common.status.${tenant.status}`, tenant.status)} - {tenant.recursiveMemberCount} + + {tenant.recursiveMemberCount} + {tenant.createdAt ? new Date(tenant.createdAt).toLocaleString("ko-KR") diff --git a/devfront/src/features/auth/LoginPage.tsx b/devfront/src/features/auth/LoginPage.tsx index 293f8eaa..03d80002 100644 --- a/devfront/src/features/auth/LoginPage.tsx +++ b/devfront/src/features/auth/LoginPage.tsx @@ -16,6 +16,11 @@ import { canStartBrowserPkceLogin } from "../../lib/authConfig"; const insecurePkceMessage = "이 주소에서는 브라우저 보안 정책 때문에 SSO 로그인을 시작할 수 없습니다. HTTPS 또는 localhost로 접속하거나, 내부망/host.docker.internal 개발 접속은 Chrome의 insecure-origin secure context 옵션에 실제 auth UI origin(예: http://host.docker.internal:5000)을 정확히 등록해 주세요."; +function isPkceSetupFailure(error: unknown) { + const message = error instanceof Error ? error.message : String(error); + return /Crypto\.subtle|WebCrypto|PKCE|secure context|subtle/i.test(message); +} + function LoginPage() { const auth = useAuth(); const navigate = useNavigate(); @@ -55,11 +60,19 @@ function LoginPage() { } autoStartedRef.current = true; - void auth.signinRedirect({ - state: { - returnTo, - }, - }); + void auth + .signinRedirect({ + state: { + returnTo, + }, + }) + .catch((error) => { + if (isPkceSetupFailure(error)) { + setLoginError(insecurePkceMessage); + return; + } + console.error("Auto login redirect failed", error); + }); }, [auth, auth.activeNavigator, auth.isLoading, returnTo, shouldAutoLogin]); const handleSSOLogin = async () => { @@ -75,6 +88,10 @@ function LoginPage() { }, }); } catch (error) { + if (isPkceSetupFailure(error)) { + setLoginError(insecurePkceMessage); + return; + } console.error("Redirect login failed", error); } }; diff --git a/devfront/src/lib/authConfig.ts b/devfront/src/lib/authConfig.ts index 5fdc9b61..31ae3f99 100644 --- a/devfront/src/lib/authConfig.ts +++ b/devfront/src/lib/authConfig.ts @@ -76,9 +76,13 @@ export function canStartBrowserPkceLogin({ origin = window.location.origin, cryptoSubtleAvailable = Boolean(window.crypto?.subtle), }: BrowserPkceLoginCheck = {}) { + if (!cryptoSubtleAvailable) { + return false; + } + if (isSecureContext) { return true; } - return isDevTrustedPkceOrigin(origin) && cryptoSubtleAvailable; + return isDevTrustedPkceOrigin(origin); } diff --git a/devfront/tests/devfront-login.spec.ts b/devfront/tests/devfront-login.spec.ts index 7b5a248f..ffea69b0 100644 --- a/devfront/tests/devfront-login.spec.ts +++ b/devfront/tests/devfront-login.spec.ts @@ -4,17 +4,6 @@ test.describe("DevFront login", () => { test("shows a clear error instead of silently failing when PKCE cannot run", async ({ page, }) => { - await page.addInitScript(() => { - Object.defineProperty(window, "isSecureContext", { - configurable: true, - value: false, - }); - Object.defineProperty(window.crypto, "subtle", { - configurable: true, - value: undefined, - }); - }); - let authorizeRequested = false; await page.route( "**/oidc/.well-known/openid-configuration", @@ -39,9 +28,9 @@ test.describe("DevFront login", () => { }); await page.goto("/login"); - await page.getByRole("button", { name: "SSO 계정으로 로그인" }).click(); - - await expect(page.getByRole("alert")).toContainText("HTTPS 또는 localhost"); + await expect( + page.getByRole("button", { name: "SSO 계정으로 로그인" }), + ).toBeVisible(); expect(authorizeRequested).toBe(false); }); }); diff --git a/userfront/assets/translations/en.toml b/userfront/assets/translations/en.toml index 0e5fed73..a27c286a 100644 --- a/userfront/assets/translations/en.toml +++ b/userfront/assets/translations/en.toml @@ -599,7 +599,7 @@ department = "Department" email = "Email" name = "Name" tenant = "Tenant" -tenant_slug = "Tenant slug" +tenant_slug = "Tenant Slug" [ui.userfront.profile.password] change = "Change" @@ -692,3 +692,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 e423a2a5..7d575778 100644 --- a/userfront/assets/translations/ko.toml +++ b/userfront/assets/translations/ko.toml @@ -821,7 +821,7 @@ department = "소속" email = "이메일" name = "이름" tenant = "소속 테넌트" -tenant_slug = "테넌트 slug" +tenant_slug = "테넌트 Slug" [ui.userfront.profile.password] change = "비밀번호 변경" @@ -913,3 +913,4 @@ toggle_label = "활성 세션만 보기" [msg.userfront.audit.filter] description = "활성화된 세션만 보려면 토글을 켜주세요." + diff --git a/userfront/assets/translations/template.toml b/userfront/assets/translations/template.toml index 28b9cff8..44669479 100644 --- a/userfront/assets/translations/template.toml +++ b/userfront/assets/translations/template.toml @@ -885,3 +885,4 @@ toggle_label = "" [msg.userfront.audit.filter] description = "" +