feat(auth): подтверждение email + актуализация SMTP/Ollama конфигов
- Резенд письма верификации (/auth/resend-verification), модалка на фронте с поллингом статуса, страница /verify-email/:token - SMTP переведён на собственный Postfix (mail.jze9mail.ru, STARTTLS, SMTP_TLS_VERIFY) вместо Yandex-заглушки в дефолтах и .env.example - OLLAMA_URL и модель в worker-gpu синхронизированы с новым GPU-хостом (llama3:8b -> qwen2.5:7b, которой раньше не было на сервере) Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
126
services/frontend/src/components/EmailVerificationModal.tsx
Normal file
126
services/frontend/src/components/EmailVerificationModal.tsx
Normal file
@@ -0,0 +1,126 @@
|
||||
import { useState } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { useMutation, useQuery } from '@tanstack/react-query';
|
||||
import { Mail, X, CheckCircle2, AlertCircle } from 'lucide-react';
|
||||
import toast from 'react-hot-toast';
|
||||
import { authApi } from '../api/client';
|
||||
import { useAuthStore } from '../store/auth';
|
||||
|
||||
export function EmailVerificationModal() {
|
||||
const { user, updateUser } = useAuthStore();
|
||||
const location = useLocation();
|
||||
const [dismissed, setDismissed] = useState(false);
|
||||
const [verified, setVerified] = useState(false);
|
||||
|
||||
// Не показывать на странице подтверждения — там своя UI
|
||||
if (location.pathname.startsWith('/verify-email')) return null;
|
||||
|
||||
const resend = useMutation({
|
||||
mutationFn: () => authApi.resendVerification(),
|
||||
onSuccess: () => toast.success('Письмо отправлено, проверьте почту'),
|
||||
onError: () => toast.error('Не удалось отправить письмо'),
|
||||
});
|
||||
|
||||
useQuery({
|
||||
queryKey: ['email-verification-poll'],
|
||||
queryFn: async () => {
|
||||
const res = await authApi.me();
|
||||
if (res.data.is_verified) {
|
||||
updateUser({ is_verified: true });
|
||||
setVerified(true);
|
||||
}
|
||||
return res.data;
|
||||
},
|
||||
enabled: !!(user && !user.is_verified && !dismissed && !verified),
|
||||
refetchInterval: 5000,
|
||||
staleTime: 0,
|
||||
});
|
||||
|
||||
if (!user || dismissed) return null;
|
||||
if (!user.is_verified && !verified) {
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40 backdrop-blur-sm">
|
||||
<div className="relative w-full max-w-md mx-4 bg-white rounded-2xl shadow-2xl p-8">
|
||||
<button
|
||||
onClick={() => setDismissed(true)}
|
||||
className="absolute top-4 right-4 text-gray-400 hover:text-gray-600 transition-colors"
|
||||
>
|
||||
<X className="w-5 h-5" />
|
||||
</button>
|
||||
|
||||
<div className="flex justify-center mb-5">
|
||||
<div className="p-4 bg-amber-50 rounded-full">
|
||||
<Mail className="w-8 h-8 text-amber-500" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2 className="text-xl font-bold text-gray-900 text-center mb-1">
|
||||
Подтвердите ваш email
|
||||
</h2>
|
||||
<p className="text-center text-sm text-gray-400 mb-6">{user.email}</p>
|
||||
|
||||
{/* Статус */}
|
||||
<div className="flex items-center justify-center gap-2 mb-6 px-4 py-3 bg-amber-50 rounded-xl">
|
||||
<AlertCircle className="w-4 h-4 text-amber-500 shrink-0" />
|
||||
<span className="text-sm font-medium text-amber-700">Email не подтверждён</span>
|
||||
</div>
|
||||
|
||||
<p className="text-sm text-gray-500 text-center mb-6 leading-relaxed">
|
||||
Мы отправили письмо на{' '}
|
||||
<span className="font-medium text-gray-800">{user.email}</span>.
|
||||
Нажмите кнопку «Подтвердить» в письме для активации аккаунта.
|
||||
</p>
|
||||
|
||||
<button
|
||||
onClick={() => resend.mutate()}
|
||||
disabled={resend.isPending}
|
||||
className="w-full py-3 bg-brand-600 text-white rounded-xl text-sm font-medium hover:bg-brand-700 disabled:opacity-50 transition-colors"
|
||||
>
|
||||
{resend.isPending ? 'Отправляем...' : 'Подтвердить'}
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => setDismissed(true)}
|
||||
className="w-full mt-3 py-2.5 text-sm text-gray-400 hover:text-gray-600 transition-colors"
|
||||
>
|
||||
Напомнить позже
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (verified) {
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40 backdrop-blur-sm">
|
||||
<div className="relative w-full max-w-md mx-4 bg-white rounded-2xl shadow-2xl p-8 text-center">
|
||||
<div className="flex justify-center mb-5">
|
||||
<div className="p-4 bg-green-50 rounded-full">
|
||||
<CheckCircle2 className="w-8 h-8 text-green-500" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2 className="text-xl font-bold text-gray-900 mb-2">Email подтверждён!</h2>
|
||||
|
||||
<div className="flex items-center justify-center gap-2 mb-6 px-4 py-3 bg-green-50 rounded-xl">
|
||||
<CheckCircle2 className="w-4 h-4 text-green-500 shrink-0" />
|
||||
<span className="text-sm font-medium text-green-700">Email подтверждён</span>
|
||||
</div>
|
||||
|
||||
<p className="text-sm text-gray-500 mb-6">
|
||||
Аккаунт активирован. Теперь вы получаете уведомления о завершении задач.
|
||||
</p>
|
||||
|
||||
<button
|
||||
onClick={() => setDismissed(true)}
|
||||
className="w-full py-3 bg-brand-600 text-white rounded-xl text-sm font-medium hover:bg-brand-700 transition-colors"
|
||||
>
|
||||
Закрыть
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import { Link, NavLink, useNavigate } from 'react-router-dom';
|
||||
import { GraduationCap, Search, BookOpen, Upload, LayoutDashboard, LogOut, User, BookMarked } from 'lucide-react';
|
||||
import { clsx } from 'clsx';
|
||||
import { useAuthStore } from '../store/auth';
|
||||
import { EmailVerificationModal } from './EmailVerificationModal';
|
||||
|
||||
interface LayoutProps {
|
||||
children: React.ReactNode;
|
||||
@@ -144,6 +145,8 @@ export function Layout({ children }: LayoutProps) {
|
||||
<main className="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
{children}
|
||||
</main>
|
||||
|
||||
<EmailVerificationModal />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user