Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions frontend/src/App.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,36 @@ describe("App, unauthenticated", () => {
}),
);
});

it("shows a session-expired screen with a working retry, not a bare error string", async () => {
// Live bug (2026-08-23): a raw IdP error string (Keycloak's literal
// "Token is not active" once the session's token expires) used to be
// the entire page -- a bare red line of un-translated text with no
// layout and no way back short of a manual reload.
mockAuth.error = new Error("Token is not active");
render(<App showLabPanels />);

expect(screen.getByText("Your session has expired.")).toBeInTheDocument();
// The raw detail stays visible for diagnostics, but is not the only content.
expect(screen.getByText("Token is not active")).toBeInTheDocument();

const button = screen.getByRole("button", { name: /log in again/i });
await userEvent.click(button);
expect(signinRedirect).toHaveBeenCalledTimes(1);
expect(signinRedirect).toHaveBeenCalledWith(
expect.objectContaining({
state: expect.objectContaining({ returnUrl: expect.stringMatching(/^\//) }),
}),
);
});

it("falls back to a generic authentication-error screen for an unrelated auth error", () => {
mockAuth.error = new Error("Network request failed");
render(<App showLabPanels />);

expect(screen.getByText("An authentication error occurred.")).toBeInTheDocument();
expect(screen.queryByText("Your session has expired.")).not.toBeInTheDocument();
});
});

function jsonResponse(body: unknown): Response {
Expand Down
41 changes: 40 additions & 1 deletion frontend/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4596,7 +4596,46 @@ export default function App({ showLabPanels = false }: { showLabPanels?: boolean
}

if (auth.error) {
return <p className="error">{t(auth.error.message)}</p>;
// A raw IdP error string (e.g. Keycloak's literal "Token is not active"
// once the session's access/refresh token expires) used to be dumped
// as the entire page: a bare red line of un-translated text with no
// layout, no explanation, and no way back in short of a manual reload.
// Session/token expiry is a routine, recoverable condition, not a
// fatal one -- give it the same login-card treatment as the normal
// login screen, with the raw detail kept as diagnostic-only text.
const isSessionExpired = /token|session/i.test(auth.error.message) &&
/expired|inactive|not active|invalid/i.test(auth.error.message);
return (
<div className="app-shell">
<main className="login-screen">
<div className="login-card">
<div className="login-header">
<h1>{brandName}</h1>
<p className="login-subtitle">
{isSessionExpired
? t("Your session has expired.")
: t("An authentication error occurred.")}
</p>
</div>
<div className="login-controls">
<button
className="btn-primary"
onClick={() => {
const returnUrl = returnUrlFromLocation();
rememberOidcReturnUrl(returnUrl);
void auth.signinRedirect({ state: { returnUrl } });
}}
>
{t("Log in again")}
</button>
</div>
<p className="login-help error" aria-label={t("Technical detail")}>
{auth.error.message}
</p>
Comment on lines +4632 to +4634

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📝 Info: aria-label can mask the error text from screen readers

The technical-detail paragraph sets aria-label on a <p> that already contains auth.error.message. Where the label is honored, it overrides the visible text as the accessible name, so assistive tech announces "Technical detail" and drops the actual error string.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

</div>
</main>
</div>
);
}

if (!auth.isAuthenticated) {
Expand Down
16 changes: 16 additions & 0 deletions frontend/src/i18n.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ const TRANSLATIONS: Partial<Record<Locale, Record<string, string>>> = {
"Loading authentication state...": "인증 상태를 불러오는 중...",
"Authenticated, but no access token was returned.": "인증되었지만 액세스 토큰이 반환되지 않았습니다.",
"Log in": "로그인",
"Log in again": "다시 로그인",
"Your session has expired.": "세션이 만료되었습니다.",
"An authentication error occurred.": "인증 오류가 발생했습니다.",
"Technical detail": "기술적 세부 정보",
"Log out": "로그아웃",
Calendar: "캘린더",
"CalDAV events": "CalDAV 이벤트",
Expand Down Expand Up @@ -373,6 +377,10 @@ const TRANSLATIONS: Partial<Record<Locale, Record<string, string>>> = {
"Loading authentication state...": "正在加载身份验证状态...",
"Authenticated, but no access token was returned.": "已完成身份验证,但未返回访问令牌。",
"Log in": "登录",
"Log in again": "重新登录",
"Your session has expired.": "您的会话已过期。",
"An authentication error occurred.": "发生身份验证错误。",
"Technical detail": "技术详情",
"Log out": "退出登录",
Calendar: "日历",
"CalDAV events": "CalDAV 事件",
Expand Down Expand Up @@ -735,6 +743,10 @@ const TRANSLATIONS: Partial<Record<Locale, Record<string, string>>> = {
"Loading authentication state...": "認証状態を読み込んでいます...",
"Authenticated, but no access token was returned.": "認証済みですが、アクセストークンが返されませんでした。",
"Log in": "ログイン",
"Log in again": "再ログイン",
"Your session has expired.": "セッションの有効期限が切れました。",
"An authentication error occurred.": "認証エラーが発生しました。",
"Technical detail": "技術的な詳細",
"Log out": "ログアウト",
Calendar: "カレンダー",
"CalDAV events": "CalDAV イベント",
Expand Down Expand Up @@ -1073,6 +1085,10 @@ const TRANSLATIONS: Partial<Record<Locale, Record<string, string>>> = {
"Loading authentication state...": "Đang tải trạng thái xác thực...",
"Authenticated, but no access token was returned.": "Đã xác thực nhưng không nhận được mã thông báo truy cập.",
"Log in": "Đăng nhập",
"Log in again": "Đăng nhập lại",
"Your session has expired.": "Phiên làm việc của bạn đã hết hạn.",
"An authentication error occurred.": "Đã xảy ra lỗi xác thực.",
"Technical detail": "Chi tiết kỹ thuật",
"Log out": "Đăng xuất",
Calendar: "Lịch",
"CalDAV events": "Sự kiện CalDAV",
Expand Down