fix: normalize auth error messages, no more raw field codes
- Add parse-api-error.js utility that handles all backend error formats:
{error:{details:{field:[msg]}}} → "Field: msg" per line
{error:{message:"field: text"}} → strips "field: " prefix
{message/detail: "text"} → direct fallback
- Apply parseApiError() on all 4 auth pages (sign-in, sign-up, forgot/reset password)
- Add whiteSpace: pre-line to Alert so multi-field errors render on separate lines
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
d5ebd2898a
commit
6aa98de721
|
|
@ -17,6 +17,7 @@ import { RouterLink } from 'src/routes/components';
|
||||||
import { Form, Field } from 'src/components/hook-form';
|
import { Form, Field } from 'src/components/hook-form';
|
||||||
|
|
||||||
import { requestPasswordReset } from 'src/auth/context/jwt';
|
import { requestPasswordReset } from 'src/auth/context/jwt';
|
||||||
|
import { parseApiError } from 'src/utils/parse-api-error';
|
||||||
|
|
||||||
// ----------------------------------------------------------------------
|
// ----------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
@ -50,8 +51,7 @@ export function JwtForgotPasswordView() {
|
||||||
setErrorMsg('');
|
setErrorMsg('');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
const msg = error?.response?.data?.error?.message || error?.response?.data?.message || error?.response?.data?.detail || 'Ошибка отправки запроса. Проверьте правильность email.';
|
setErrorMsg(parseApiError(error, 'Ошибка отправки запроса. Проверьте правильность email.'));
|
||||||
setErrorMsg(msg);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -72,7 +72,7 @@ export function JwtForgotPasswordView() {
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{!!errorMsg && (
|
{!!errorMsg && (
|
||||||
<Alert severity="error" sx={{ mb: 3 }}>
|
<Alert severity="error" sx={{ mb: 3, whiteSpace: 'pre-line' }}>
|
||||||
{errorMsg}
|
{errorMsg}
|
||||||
</Alert>
|
</Alert>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ import { Iconify } from 'src/components/iconify';
|
||||||
import { Form, Field } from 'src/components/hook-form';
|
import { Form, Field } from 'src/components/hook-form';
|
||||||
|
|
||||||
import { confirmPasswordReset } from 'src/auth/context/jwt';
|
import { confirmPasswordReset } from 'src/auth/context/jwt';
|
||||||
|
import { parseApiError } from 'src/utils/parse-api-error';
|
||||||
|
|
||||||
// ----------------------------------------------------------------------
|
// ----------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
@ -76,8 +77,7 @@ function ResetPasswordContent() {
|
||||||
setErrorMsg('');
|
setErrorMsg('');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
const msg = error?.response?.data?.error?.message || error?.response?.data?.message || error?.response?.data?.detail || 'Не удалось сбросить пароль. Возможно, ссылка устарела — запросите новую.';
|
setErrorMsg(parseApiError(error, 'Не удалось сбросить пароль. Возможно, ссылка устарела — запросите новую.'));
|
||||||
setErrorMsg(msg);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -123,7 +123,7 @@ function ResetPasswordContent() {
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
||||||
{!!errorMsg && (
|
{!!errorMsg && (
|
||||||
<Alert severity="error" sx={{ mb: 3 }}>
|
<Alert severity="error" sx={{ mb: 3, whiteSpace: 'pre-line' }}>
|
||||||
{errorMsg}
|
{errorMsg}
|
||||||
</Alert>
|
</Alert>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@ import { Form, Field } from 'src/components/hook-form';
|
||||||
|
|
||||||
import { useAuthContext } from 'src/auth/hooks';
|
import { useAuthContext } from 'src/auth/hooks';
|
||||||
import { signInWithPassword } from 'src/auth/context/jwt';
|
import { signInWithPassword } from 'src/auth/context/jwt';
|
||||||
|
import { parseApiError } from 'src/utils/parse-api-error';
|
||||||
|
|
||||||
// ----------------------------------------------------------------------
|
// ----------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
@ -73,7 +74,7 @@ export function JwtSignInView() {
|
||||||
router.replace(returnTo);
|
router.replace(returnTo);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
setErrorMsg(error instanceof Error ? error.message : error);
|
setErrorMsg(parseApiError(error, 'Неверный email или пароль'));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -145,7 +146,7 @@ export function JwtSignInView() {
|
||||||
{renderHead}
|
{renderHead}
|
||||||
|
|
||||||
{!!errorMsg && (
|
{!!errorMsg && (
|
||||||
<Alert severity="error" sx={{ mb: 3 }}>
|
<Alert severity="error" sx={{ mb: 3, whiteSpace: 'pre-line' }}>
|
||||||
{errorMsg}
|
{errorMsg}
|
||||||
</Alert>
|
</Alert>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,7 @@ import { Iconify } from 'src/components/iconify';
|
||||||
import { Form, Field } from 'src/components/hook-form';
|
import { Form, Field } from 'src/components/hook-form';
|
||||||
|
|
||||||
import { signUp } from 'src/auth/context/jwt';
|
import { signUp } from 'src/auth/context/jwt';
|
||||||
|
import { parseApiError } from 'src/utils/parse-api-error';
|
||||||
import { useAuthContext } from 'src/auth/hooks';
|
import { useAuthContext } from 'src/auth/hooks';
|
||||||
import { searchCities } from 'src/utils/profile-api';
|
import { searchCities } from 'src/utils/profile-api';
|
||||||
|
|
||||||
|
|
@ -178,8 +179,7 @@ export function JwtSignUpView() {
|
||||||
router.replace(paths.dashboard.root);
|
router.replace(paths.dashboard.root);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
const msg = error?.response?.data?.error?.message || error?.response?.data?.message || error?.response?.data?.detail || (error instanceof Error ? error.message : 'Ошибка регистрации. Проверьте введённые данные.');
|
setErrorMsg(parseApiError(error, 'Ошибка регистрации. Проверьте введённые данные.'));
|
||||||
setErrorMsg(msg);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -310,7 +310,7 @@ export function JwtSignUpView() {
|
||||||
{renderHead}
|
{renderHead}
|
||||||
|
|
||||||
{!!errorMsg && (
|
{!!errorMsg && (
|
||||||
<Alert severity="error" sx={{ mb: 3 }}>
|
<Alert severity="error" sx={{ mb: 3, whiteSpace: 'pre-line' }}>
|
||||||
{errorMsg}
|
{errorMsg}
|
||||||
</Alert>
|
</Alert>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,57 @@
|
||||||
|
/**
|
||||||
|
* Парсит ошибки API и возвращает читаемую строку.
|
||||||
|
*
|
||||||
|
* Форматы бэкенда:
|
||||||
|
* { error: { message: "field: text", details: { field: ["text"] } } }
|
||||||
|
* { message: "text" }
|
||||||
|
* { detail: "text" }
|
||||||
|
*/
|
||||||
|
|
||||||
|
const FIELD_LABELS = {
|
||||||
|
email: 'Email',
|
||||||
|
password: 'Пароль',
|
||||||
|
password_confirm: 'Подтверждение пароля',
|
||||||
|
new_password: 'Новый пароль',
|
||||||
|
new_password_confirm: 'Подтверждение пароля',
|
||||||
|
first_name: 'Имя',
|
||||||
|
last_name: 'Фамилия',
|
||||||
|
city: 'Город',
|
||||||
|
timezone: 'Часовой пояс',
|
||||||
|
token: 'Токен',
|
||||||
|
non_field_errors: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
function label(field) {
|
||||||
|
return FIELD_LABELS[field] ?? field;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseApiError(error, fallback = 'Произошла ошибка. Попробуйте ещё раз.') {
|
||||||
|
const data = error?.response?.data;
|
||||||
|
if (!data) return error?.message || fallback;
|
||||||
|
|
||||||
|
// { error: { details: { field: ["msg"] } } }
|
||||||
|
const details = data?.error?.details;
|
||||||
|
if (details && typeof details === 'object') {
|
||||||
|
const lines = Object.entries(details)
|
||||||
|
.flatMap(([field, msgs]) => {
|
||||||
|
const lbl = label(field);
|
||||||
|
const arr = Array.isArray(msgs) ? msgs : [String(msgs)];
|
||||||
|
return arr.map((m) => (lbl ? `${lbl}: ${m}` : m));
|
||||||
|
});
|
||||||
|
if (lines.length) return lines.join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
// { error: { message: "field: text" } } — strip "field: " prefix
|
||||||
|
const errMsg = data?.error?.message;
|
||||||
|
if (errMsg) {
|
||||||
|
// Remove leading "fieldname: " pattern
|
||||||
|
return errMsg.replace(/^[\w_]+:\s+/, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Flat formats
|
||||||
|
if (data?.message) return data.message;
|
||||||
|
if (data?.detail) return data.detail;
|
||||||
|
if (typeof data === 'string') return data;
|
||||||
|
|
||||||
|
return error?.message || fallback;
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue