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:
Dev Server 2026-03-12 17:15:19 +03:00
parent d5ebd2898a
commit 6aa98de721
5 changed files with 69 additions and 11 deletions

View File

@ -17,6 +17,7 @@ import { RouterLink } from 'src/routes/components';
import { Form, Field } from 'src/components/hook-form';
import { requestPasswordReset } from 'src/auth/context/jwt';
import { parseApiError } from 'src/utils/parse-api-error';
// ----------------------------------------------------------------------
@ -50,8 +51,7 @@ export function JwtForgotPasswordView() {
setErrorMsg('');
} catch (error) {
console.error(error);
const msg = error?.response?.data?.error?.message || error?.response?.data?.message || error?.response?.data?.detail || 'Ошибка отправки запроса. Проверьте правильность email.';
setErrorMsg(msg);
setErrorMsg(parseApiError(error, 'Ошибка отправки запроса. Проверьте правильность email.'));
}
});
@ -72,7 +72,7 @@ export function JwtForgotPasswordView() {
)}
{!!errorMsg && (
<Alert severity="error" sx={{ mb: 3 }}>
<Alert severity="error" sx={{ mb: 3, whiteSpace: 'pre-line' }}>
{errorMsg}
</Alert>
)}

View File

@ -22,6 +22,7 @@ import { Iconify } from 'src/components/iconify';
import { Form, Field } from 'src/components/hook-form';
import { confirmPasswordReset } from 'src/auth/context/jwt';
import { parseApiError } from 'src/utils/parse-api-error';
// ----------------------------------------------------------------------
@ -76,8 +77,7 @@ function ResetPasswordContent() {
setErrorMsg('');
} catch (error) {
console.error(error);
const msg = error?.response?.data?.error?.message || error?.response?.data?.message || error?.response?.data?.detail || 'Не удалось сбросить пароль. Возможно, ссылка устарела — запросите новую.';
setErrorMsg(msg);
setErrorMsg(parseApiError(error, 'Не удалось сбросить пароль. Возможно, ссылка устарела — запросите новую.'));
}
});
@ -123,7 +123,7 @@ function ResetPasswordContent() {
</Stack>
{!!errorMsg && (
<Alert severity="error" sx={{ mb: 3 }}>
<Alert severity="error" sx={{ mb: 3, whiteSpace: 'pre-line' }}>
{errorMsg}
</Alert>
)}

View File

@ -23,6 +23,7 @@ import { Form, Field } from 'src/components/hook-form';
import { useAuthContext } from 'src/auth/hooks';
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);
} catch (error) {
console.error(error);
setErrorMsg(error instanceof Error ? error.message : error);
setErrorMsg(parseApiError(error, 'Неверный email или пароль'));
}
});
@ -145,7 +146,7 @@ export function JwtSignInView() {
{renderHead}
{!!errorMsg && (
<Alert severity="error" sx={{ mb: 3 }}>
<Alert severity="error" sx={{ mb: 3, whiteSpace: 'pre-line' }}>
{errorMsg}
</Alert>
)}

View File

@ -28,6 +28,7 @@ import { Iconify } from 'src/components/iconify';
import { Form, Field } from 'src/components/hook-form';
import { signUp } from 'src/auth/context/jwt';
import { parseApiError } from 'src/utils/parse-api-error';
import { useAuthContext } from 'src/auth/hooks';
import { searchCities } from 'src/utils/profile-api';
@ -178,8 +179,7 @@ export function JwtSignUpView() {
router.replace(paths.dashboard.root);
} catch (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(msg);
setErrorMsg(parseApiError(error, 'Ошибка регистрации. Проверьте введённые данные.'));
}
});
@ -310,7 +310,7 @@ export function JwtSignUpView() {
{renderHead}
{!!errorMsg && (
<Alert severity="error" sx={{ mb: 3 }}>
<Alert severity="error" sx={{ mb: 3, whiteSpace: 'pre-line' }}>
{errorMsg}
</Alert>
)}

View File

@ -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;
}