fix(auth-ui): тихий фейл 400 при смене пароля, валидация, пароль при создании

PLAN-user-password-fixes.md (инцидент 2026-09-07, user 55/56):
- toServiceError в client auth.service: BadRequestError → {success:false, error}
- полная клиентская валидация пароля в ProfilePasswordRow
- опциональный пароль при создании пользователя (UserModal + POST /api/users)
- onError с toast в PollCard/ReactionBar (этап 4)
- 11 новых тестов (129/129)
This commit is contained in:
2026-09-08 16:42:59 +03:00
parent 0b2319b11d
commit c8d7f9b336
4 changed files with 182 additions and 0 deletions

View File

@@ -3,6 +3,7 @@ import { cn } from '@/lib/utils';
import { useMutation } from '@tanstack/react-query';
import { apiRequest } from '@/lib/queryClient';
import { useAuth } from '@/hooks/useAuth';
import { useToast } from '@/hooks/use-toast';
import { isAdmin } from '@/lib/permissions';
export interface PollData {
@@ -26,10 +27,19 @@ interface PollCardProps {
export function PollCard({ poll, onPollChange, isOwn }: PollCardProps) {
const { user } = useAuth();
const { toast } = useToast();
const canClose = poll.createdBy === user?.id || isAdmin(user);
const hasVoted = poll.myVote !== null && poll.myVote.length > 0;
const showResults = hasVoted || poll.isClosed;
// apiRequest при 400/409 бросает ошибку с текстом сервера в message; сетевые TypeError не показываем
const onPollError = (error: unknown) =>
toast({
title: 'Ошибка голосования',
description: error instanceof Error && !(error instanceof TypeError) ? error.message : undefined,
variant: 'destructive',
});
const voteMutation = useMutation({
mutationFn: async (optionIndexes: number[]) => {
const res = await apiRequest('POST', `/api/polls/${poll.id}/vote`, { optionIndexes });
@@ -38,6 +48,7 @@ export function PollCard({ poll, onPollChange, isOwn }: PollCardProps) {
return data.poll as PollData;
},
onSuccess: onPollChange,
onError: onPollError,
});
const unvoteMutation = useMutation({
@@ -48,6 +59,7 @@ export function PollCard({ poll, onPollChange, isOwn }: PollCardProps) {
return data.poll as PollData;
},
onSuccess: onPollChange,
onError: onPollError,
});
const closeMutation = useMutation({
@@ -58,6 +70,7 @@ export function PollCard({ poll, onPollChange, isOwn }: PollCardProps) {
return data.poll as PollData;
},
onSuccess: onPollChange,
onError: onPollError,
});
const handleOptionClick = (idx: number) => {

View File

@@ -2,6 +2,7 @@ import { cn } from '@/lib/utils';
import { EmojiPicker } from './EmojiPicker';
import { useMutation } from '@tanstack/react-query';
import { apiRequest } from '@/lib/queryClient';
import { useToast } from '@/hooks/use-toast';
import {
Popover,
PopoverContent,
@@ -39,6 +40,8 @@ export function ReactionBar({
? `/api/messages/${messageId}/reactions`
: `/api/conv-messages/${messageId}/reactions`;
const { toast } = useToast();
const toggleMutation = useMutation({
mutationFn: async (emoji: string) => {
const res = await apiRequest('POST', endpoint, { emoji });
@@ -49,6 +52,12 @@ export function ReactionBar({
onSuccess: (updated) => {
onReactionsChange(updated);
},
// apiRequest при 400/409 бросает ошибку с текстом сервера в message; сетевые TypeError не показываем
onError: (error) => toast({
title: 'Не удалось поставить реакцию',
description: error instanceof Error && !(error instanceof TypeError) ? error.message : undefined,
variant: 'destructive',
}),
});
const handleEmoji = (emoji: string) => {