From 6aa98de721b1a2ec696665577170ac2c79275b51 Mon Sep 17 00:00:00 2001 From: Dev Server Date: Thu, 12 Mar 2026 17:15:19 +0300 Subject: [PATCH] fix: normalize auth error messages, no more raw field codes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- .../auth/jwt/jwt-forgot-password-view.jsx | 6 +- .../auth/jwt/jwt-reset-password-view.jsx | 6 +- .../sections/auth/jwt/jwt-sign-in-view.jsx | 5 +- .../sections/auth/jwt/jwt-sign-up-view.jsx | 6 +- front_minimal/src/utils/parse-api-error.js | 57 +++++++++++++++++++ 5 files changed, 69 insertions(+), 11 deletions(-) create mode 100644 front_minimal/src/utils/parse-api-error.js diff --git a/front_minimal/src/sections/auth/jwt/jwt-forgot-password-view.jsx b/front_minimal/src/sections/auth/jwt/jwt-forgot-password-view.jsx index bad5fab..4f0d6a9 100644 --- a/front_minimal/src/sections/auth/jwt/jwt-forgot-password-view.jsx +++ b/front_minimal/src/sections/auth/jwt/jwt-forgot-password-view.jsx @@ -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 && ( - + {errorMsg} )} diff --git a/front_minimal/src/sections/auth/jwt/jwt-reset-password-view.jsx b/front_minimal/src/sections/auth/jwt/jwt-reset-password-view.jsx index 9daccc8..d9214e9 100644 --- a/front_minimal/src/sections/auth/jwt/jwt-reset-password-view.jsx +++ b/front_minimal/src/sections/auth/jwt/jwt-reset-password-view.jsx @@ -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() { {!!errorMsg && ( - + {errorMsg} )} diff --git a/front_minimal/src/sections/auth/jwt/jwt-sign-in-view.jsx b/front_minimal/src/sections/auth/jwt/jwt-sign-in-view.jsx index a6c0d41..95ce3d6 100644 --- a/front_minimal/src/sections/auth/jwt/jwt-sign-in-view.jsx +++ b/front_minimal/src/sections/auth/jwt/jwt-sign-in-view.jsx @@ -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 && ( - + {errorMsg} )} diff --git a/front_minimal/src/sections/auth/jwt/jwt-sign-up-view.jsx b/front_minimal/src/sections/auth/jwt/jwt-sign-up-view.jsx index ec521a8..f4f6790 100644 --- a/front_minimal/src/sections/auth/jwt/jwt-sign-up-view.jsx +++ b/front_minimal/src/sections/auth/jwt/jwt-sign-up-view.jsx @@ -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 && ( - + {errorMsg} )} diff --git a/front_minimal/src/utils/parse-api-error.js b/front_minimal/src/utils/parse-api-error.js new file mode 100644 index 0000000..7b87277 --- /dev/null +++ b/front_minimal/src/utils/parse-api-error.js @@ -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; +}