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