feat(login): surface SSO callback failures via error query param
The CAS / OIDC / SAML callbacks now redirect to `/login?error=<code>` 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.
This commit is contained in:
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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': 'Введите пароль',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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': '请输入密码',
|
||||
|
||||
@@ -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=<code>`` 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<string, string> = {
|
||||
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,
|
||||
|
||||
Reference in New Issue
Block a user