From 1e97d29fca254dcaf9cea1c57bc9294a944acd2e Mon Sep 17 00:00:00 2001 From: gitlawr Date: Tue, 30 Jun 2026 19:13:14 +0800 Subject: [PATCH] feat(login): surface SSO callback failures via error query param MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The CAS / OIDC / SAML callbacks now redirect to `/login?error=` on failure instead of letting the browser land on a raw JSON error page, so the actionable copy reaches the user. Two codes are recognised: * `source_conflict` — incoming SSO username collides with an existing account from a different source. Message points the user at an administrator to link or convert. * `auth_failed` — anything else (bad ticket, expired state, IdP unreachable, malformed response). Generic message: try again or contact the administrator. On mount the login form picks up the `?error=` query param, maps it through a small `messageIdByCode` table to an i18n key, and routes the result through the existing auth-error toast. Unknown codes are silently ignored so a future server release adding a code doesn't render a bare key. The query param is cleared via `history.replaceState` so a refresh doesn't re-fire the toast. Strings added to all five locales. --- src/locales/en-US/common.ts | 4 +++ src/locales/ja-JP/common.ts | 4 +++ src/locales/ru-RU/common.ts | 4 +++ src/locales/tr-TR/common.ts | 4 +++ src/locales/zh-CN/common.ts | 3 ++ src/pages/login/components/login-form.tsx | 38 +++++++++++++++++++++-- 6 files changed, 55 insertions(+), 2 deletions(-) diff --git a/src/locales/en-US/common.ts b/src/locales/en-US/common.ts index b44c87d3..07c35d43 100644 --- a/src/locales/en-US/common.ts +++ b/src/locales/en-US/common.ts @@ -267,6 +267,10 @@ export default { 'common.select.count': '{count} selected', 'common.login.auth': 'Authenticating...', 'common.login.auth.failed': 'Authentication failed', + 'common.login.error.source_conflict': + 'An account with this username already exists from a different authentication source. Please contact an administrator to link or convert it.', + 'common.login.error.auth_failed': + 'Authentication with the identity provider failed. Please try again or contact your administrator.', 'common.login.password': 'Log in with Password', 'common.login.username.holder': 'Please enter username', 'common.login.password.holder': 'Please enter password', diff --git a/src/locales/ja-JP/common.ts b/src/locales/ja-JP/common.ts index 22bfb2a6..25af4459 100644 --- a/src/locales/ja-JP/common.ts +++ b/src/locales/ja-JP/common.ts @@ -266,6 +266,10 @@ export default { 'common.select.count': '{count} selected', 'common.login.auth': 'Authenticating...', 'common.login.auth.failed': 'Authentication failed', + 'common.login.error.source_conflict': + 'このユーザー名のアカウントは別の認証ソースで既に存在します。管理者にリンクまたは変換を依頼してください。', + 'common.login.error.auth_failed': + 'ID プロバイダーでの認証に失敗しました。再試行するか、管理者にお問い合わせください。', 'common.login.password': 'Log in with Password', 'common.login.username.holder': 'Please enter username', 'common.login.password.holder': 'Please enter password', diff --git a/src/locales/ru-RU/common.ts b/src/locales/ru-RU/common.ts index 9af9534d..8df477ba 100644 --- a/src/locales/ru-RU/common.ts +++ b/src/locales/ru-RU/common.ts @@ -265,6 +265,10 @@ export default { 'common.select.count': '{count} Выбрано', 'common.login.auth': 'Аутентификация...', 'common.login.auth.failed': 'Ошибка аутентификации', + 'common.login.error.source_conflict': + 'Учётная запись с таким именем уже существует, но с другим источником аутентификации. Обратитесь к администратору для связывания или преобразования.', + 'common.login.error.auth_failed': + 'Не удалось пройти аутентификацию через провайдера идентификации. Попробуйте ещё раз или обратитесь к администратору.', 'common.login.password': 'Войти с паролем', 'common.login.username.holder': 'Введите имя пользователя', 'common.login.password.holder': 'Введите пароль', diff --git a/src/locales/tr-TR/common.ts b/src/locales/tr-TR/common.ts index e0383b1c..62de4f36 100644 --- a/src/locales/tr-TR/common.ts +++ b/src/locales/tr-TR/common.ts @@ -269,6 +269,10 @@ export default { 'common.select.count': '{count} seçildi', 'common.login.auth': 'Kimlik doğrulanıyor...', 'common.login.auth.failed': 'Kimlik doğrulama başarısız', + 'common.login.error.source_conflict': + 'Bu kullanıcı adıyla farklı bir kimlik doğrulama kaynağından bir hesap zaten mevcut. Bağlamak veya dönüştürmek için lütfen yöneticinize başvurun.', + 'common.login.error.auth_failed': + 'Kimlik sağlayıcısı ile kimlik doğrulama başarısız oldu. Lütfen tekrar deneyin veya yöneticinize başvurun.', 'common.login.password': 'Şifre ile giriş yap', 'common.login.username.holder': 'Lütfen kullanıcı adını girin', 'common.login.password.holder': 'Lütfen şifreyi girin', diff --git a/src/locales/zh-CN/common.ts b/src/locales/zh-CN/common.ts index 49a5b8a6..44d26e0b 100644 --- a/src/locales/zh-CN/common.ts +++ b/src/locales/zh-CN/common.ts @@ -257,6 +257,9 @@ export default { 'common.select.count': '已选 {count} 项', 'common.login.auth': '认证中...', 'common.login.auth.failed': '认证失败', + 'common.login.error.source_conflict': + '已存在同名账号但来源不同。请联系管理员关联或转换该账号。', + 'common.login.error.auth_failed': '身份提供商认证失败。请重试或联系管理员。', 'common.login.password': '使用密码登录', 'common.login.username.holder': '请输入用户名', 'common.login.password.holder': '请输入密码', diff --git a/src/pages/login/components/login-form.tsx b/src/pages/login/components/login-form.tsx index 5139d325..7532aa69 100644 --- a/src/pages/login/components/login-form.tsx +++ b/src/pages/login/components/login-form.tsx @@ -1,10 +1,10 @@ import LogoIcon from '@/assets/images/gpustack-logo.png'; import { userAtom } from '@/atoms/user'; -import { useIntl, useModel } from '@umijs/max'; +import { history, useIntl, useModel } from '@umijs/max'; import { Button, Divider, Form, Spin, message } from 'antd'; import { createStyles } from 'antd-style'; import { useAtom } from 'jotai'; -import { useMemo, useState } from 'react'; +import { useEffect, useMemo, useState } from 'react'; import { flushSync } from 'react-dom'; import styled from 'styled-components'; import { useLocalAuth } from '../hooks/use-local-auth'; @@ -148,6 +148,40 @@ const LoginForm = () => { }); }; + // SSO callbacks (CAS / OIDC / SAML) are full-page IdP redirects, so + // a JSON exception raised in the callback lands the browser on a + // raw error page rather than this form. The backend instead + // redirects to ``/login?error=`` on the failure modes that + // deserve user-facing copy — read the code on mount, route it + // through the existing toast, and clear the param from the URL so + // a refresh doesn't re-fire the toast. + useEffect(() => { + const params = new URLSearchParams(window.location.search); + const errorCode = params.get('error'); + if (!errorCode) return; + // Codes recognised by the backend's SSO callback wrapper. Map to + // an i18n key per code; an unknown code is silently ignored so a + // future server release adding a code doesn't render a bare key. + const messageIdByCode: Record = { + source_conflict: 'common.login.error.source_conflict', + auth_failed: 'common.login.error.auth_failed' + }; + const messageId = messageIdByCode[errorCode]; + if (messageId) { + handleOnError(new Error(intl.formatMessage({ id: messageId }))); + } + params.delete('error'); + const newQuery = params.toString(); + // Route through ``@umijs/max``'s ``history`` rather than + // ``window.history.replaceState`` so the router's internal + // location stays in sync with the address bar — any other code + // that reads it (route guards, ``useLocation``, …) sees the + // cleaned URL on the same render. + history.replace( + window.location.pathname + (newQuery ? `?${newQuery}` : '') + ); + }, []); + // local user authentication const { handleLogin, submitLoading } = useLocalAuth({ fetchUserInfo,