From f5bd2544c939de0aa53bb3fba0357579ff2c8eb9 Mon Sep 17 00:00:00 2001 From: Egor Date: Fri, 23 Jan 2026 11:32:52 +0300 Subject: [PATCH 01/26] Add files via upload --- src/locales/en.json | 14 ++++++++++++-- src/locales/fa.json | 1 + src/locales/ru.json | 12 +++++++++++- src/locales/zh.json | 1 + 4 files changed, 25 insertions(+), 3 deletions(-) diff --git a/src/locales/en.json b/src/locales/en.json index df51eb8..77f3704 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -60,7 +60,9 @@ "logout": "Logout", "email": "Email", "password": "Password", - "confirmPassword": "Confirm password", + "confirmPassword": "Confirm Password", + "firstName": "First Name", + "firstNamePlaceholder": "Your name (optional)", "forgotPassword": "Forgot password?", "resetPassword": "Reset password", "loginWithTelegram": "Login with Telegram", @@ -76,7 +78,14 @@ "tryAgain": "Try Again", "welcomeBack": "Welcome back!", "loginTitle": "Login to Cabinet", - "loginSubtitle": "Sign in with Telegram or use your email" + "loginSubtitle": "Sign in with Telegram or use your email", + "passwordMismatch": "Passwords do not match", + "passwordTooShort": "Password must be at least 8 characters", + "invalidEmail": "Please enter a valid email address", + "emailAlreadyRegistered": "This email is already registered", + "invalidCredentials": "Invalid email or password", + "tooManyAttempts": "Too many attempts. Please try again later", + "verificationEmailNotice": "After registration, a verification email will be sent to your address" }, "emailVerification": { "title": "Email Verification", @@ -1187,6 +1196,7 @@ "linkEmailDescription": "Link your email to log in without Telegram. After linking, you'll receive a verification email.", "linkEmail": "Link Email", "emailRequired": "Email is required", + "invalidEmail": "Please enter a valid email address", "passwordMinLength": "Password must be at least 8 characters", "passwordsMismatch": "Passwords do not match", "passwordPlaceholder": "Enter password", diff --git a/src/locales/fa.json b/src/locales/fa.json index 39580de..b03b8b6 100644 --- a/src/locales/fa.json +++ b/src/locales/fa.json @@ -669,6 +669,7 @@ "linkEmailDescription": "ایمیل خود را متصل کنید تا بدون تلگرام وارد شوید. پس از اتصال، ایمیل تایید دریافت خواهید کرد.", "linkEmail": "اتصال ایمیل", "emailRequired": "ایمیل الزامی است", + "invalidEmail": "لطفا یک آدرس ایمیل معتبر وارد کنید", "passwordMinLength": "رمز عبور باید حداقل 8 کاراکتر باشد", "passwordsMismatch": "رمزهای عبور مطابقت ندارند", "passwordPlaceholder": "رمز عبور را وارد کنید", diff --git a/src/locales/ru.json b/src/locales/ru.json index f88820b..5ac0cda 100644 --- a/src/locales/ru.json +++ b/src/locales/ru.json @@ -61,6 +61,8 @@ "email": "Email", "password": "Пароль", "confirmPassword": "Подтвердите пароль", + "firstName": "Имя", + "firstNamePlaceholder": "Ваше имя (необязательно)", "forgotPassword": "Забыли пароль?", "resetPassword": "Сбросить пароль", "loginWithTelegram": "Войти через Telegram", @@ -76,7 +78,14 @@ "tryAgain": "Попробовать снова", "welcomeBack": "Добро пожаловать!", "loginTitle": "Вход в личный кабинет", - "loginSubtitle": "Войдите через Telegram или используйте email" + "loginSubtitle": "Войдите через Telegram или используйте email", + "passwordMismatch": "Пароли не совпадают", + "passwordTooShort": "Пароль должен быть минимум 8 символов", + "invalidEmail": "Введите корректный email адрес", + "emailAlreadyRegistered": "Этот email уже зарегистрирован", + "invalidCredentials": "Неверный email или пароль", + "tooManyAttempts": "Слишком много попыток. Попробуйте позже", + "verificationEmailNotice": "После регистрации на вашу почту будет отправлено письмо для подтверждения" }, "emailVerification": { "title": "Подтверждение email", @@ -1880,6 +1889,7 @@ "linkEmailDescription": "Привяжите email для входа без Telegram. После привязки на ваш email придёт письмо для подтверждения.", "linkEmail": "Привязать Email", "emailRequired": "Введите email", + "invalidEmail": "Введите корректный email адрес", "passwordMinLength": "Пароль должен быть минимум 8 символов", "passwordsMismatch": "Пароли не совпадают", "passwordPlaceholder": "Введите пароль", diff --git a/src/locales/zh.json b/src/locales/zh.json index 2f3b724..42a892b 100644 --- a/src/locales/zh.json +++ b/src/locales/zh.json @@ -670,6 +670,7 @@ "linkEmailDescription": "绑定邮箱以便无需Telegram登录。绑定后将收到验证邮件。", "linkEmail": "绑定邮箱", "emailRequired": "请输入邮箱", + "invalidEmail": "请输入有效的电子邮件地址", "passwordMinLength": "密码至少8个字符", "passwordsMismatch": "密码不匹配", "passwordPlaceholder": "输入密码", From c8d94251e9a0da1139c92f4673c232f93b4f5b58 Mon Sep 17 00:00:00 2001 From: Egor Date: Fri, 23 Jan 2026 11:33:26 +0300 Subject: [PATCH 02/26] Add files via upload --- src/pages/Login.tsx | 221 +++++++++++++++++++++++++++++++----------- src/pages/Profile.tsx | 6 +- 2 files changed, 168 insertions(+), 59 deletions(-) diff --git a/src/pages/Login.tsx b/src/pages/Login.tsx index a3683a3..90e7cdc 100644 --- a/src/pages/Login.tsx +++ b/src/pages/Login.tsx @@ -12,10 +12,13 @@ export default function Login() { const { t } = useTranslation() const navigate = useNavigate() const location = useLocation() - const { isAuthenticated, loginWithTelegram, loginWithEmail } = useAuthStore() + const { isAuthenticated, loginWithTelegram, loginWithEmail, registerWithEmail } = useAuthStore() const [activeTab, setActiveTab] = useState<'telegram' | 'email'>('telegram') + const [authMode, setAuthMode] = useState<'login' | 'register'>('login') const [email, setEmail] = useState('') const [password, setPassword] = useState('') + const [confirmPassword, setConfirmPassword] = useState('') + const [firstName, setFirstName] = useState('') const [error, setError] = useState('') const [isLoading, setIsLoading] = useState(false) const [isTelegramWebApp, setIsTelegramWebApp] = useState(false) @@ -94,22 +97,49 @@ export default function Login() { tryTelegramAuth() }, [loginWithTelegram, navigate, t, getReturnUrl]) - const handleEmailLogin = async (e: React.FormEvent) => { + const handleEmailSubmit = async (e: React.FormEvent) => { e.preventDefault() setError('') + + // Валидация email + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/ + if (!email.trim() || !emailRegex.test(email.trim())) { + setError(t('auth.invalidEmail', 'Please enter a valid email address')) + return + } + + if (authMode === 'register') { + // Валидация для регистрации + if (password !== confirmPassword) { + setError(t('auth.passwordMismatch', 'Passwords do not match')) + return + } + if (password.length < 8) { + setError(t('auth.passwordTooShort', 'Password must be at least 8 characters')) + return + } + } + setIsLoading(true) try { - await loginWithEmail(email, password) + if (authMode === 'login') { + await loginWithEmail(email, password) + } else { + await registerWithEmail(email, password, firstName || undefined) + } navigate(getReturnUrl(), { replace: true }) } catch (err: unknown) { const error = err as { response?: { status?: number; data?: { detail?: string } } } - // Show user-friendly error messages without exposing sensitive server details const status = error.response?.status - if (status === 401 || status === 403) { - setError(t('auth.invalidCredentials', 'Неверный email или пароль')) + const detail = error.response?.data?.detail + + if (status === 400 && detail?.includes('already registered')) { + setError(t('auth.emailAlreadyRegistered', 'This email is already registered')) + } else if (status === 401 || status === 403) { + setError(t('auth.invalidCredentials', 'Invalid email or password')) } else if (status === 429) { - setError(t('auth.tooManyAttempts', 'Слишком много попыток. Попробуйте позже')) + setError(t('auth.tooManyAttempts', 'Too many attempts. Please try again later')) } else { setError(t('common.error')) } @@ -205,60 +235,137 @@ export default function Login() { )} ) : ( -
-
- - setEmail(e.target.value)} - /> +
+ {/* Login / Register toggle */} +
+ +
-
- - setPassword(e.target.value)} - /> -
- - -

- {t('auth.registerHint')} -

- +
+ + setEmail(e.target.value)} + /> +
+ +
+ + setPassword(e.target.value)} + /> +
+ + {/* Confirm password - only for registration */} + {authMode === 'register' && ( +
+ + setConfirmPassword(e.target.value)} + /> +
+ )} + + + + + {/* Verification notice for registration */} + {authMode === 'register' && ( +

+ {t('auth.verificationEmailNotice', 'After registration, a verification email will be sent to your address')} +

+ )} + + {/* Forgot password link - only for login */} + {authMode === 'login' && ( +

+ {t('auth.registerHint')} +

+ )} +
)}
diff --git a/src/pages/Profile.tsx b/src/pages/Profile.tsx index a2d4b83..ddc9079 100644 --- a/src/pages/Profile.tsx +++ b/src/pages/Profile.tsx @@ -74,8 +74,10 @@ export default function Profile() { setError(null) setSuccess(null) - if (!email.trim()) { - setError(t('profile.emailRequired')) + // Валидация email + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/ + if (!email.trim() || !emailRegex.test(email.trim())) { + setError(t('profile.invalidEmail', 'Please enter a valid email address')) return } From 0cf0518be24ff5c7a4cf61805b39f97eab8c0877 Mon Sep 17 00:00:00 2001 From: Egor Date: Fri, 23 Jan 2026 11:33:50 +0300 Subject: [PATCH 03/26] Update auth.ts --- src/store/auth.ts | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/store/auth.ts b/src/store/auth.ts index a31b075..ccee53d 100644 --- a/src/store/auth.ts +++ b/src/store/auth.ts @@ -33,6 +33,7 @@ interface AuthState { loginWithTelegram: (initData: string) => Promise loginWithTelegramWidget: (data: TelegramWidgetData) => Promise loginWithEmail: (email: string, password: string) => Promise + registerWithEmail: (email: string, password: string, firstName?: string) => Promise } // Блокировка для предотвращения race condition при инициализации @@ -260,6 +261,23 @@ export const useAuthStore = create()( }) get().checkAdminStatus() }, + + registerWithEmail: async (email, password, firstName) => { + const response = await authApi.registerEmailStandalone({ + email, + password, + first_name: firstName, + language: navigator.language.split('-')[0] || 'ru', + }) + tokenStorage.setTokens(response.access_token, response.refresh_token) + set({ + accessToken: response.access_token, + refreshToken: response.refresh_token, + user: response.user, + isAuthenticated: true, + }) + get().checkAdminStatus() + }, }), { name: 'cabinet-auth', From 3a5034cb1ed3dd07d8bcd80b30664a6dce635a4f Mon Sep 17 00:00:00 2001 From: Egor Date: Fri, 23 Jan 2026 11:34:14 +0300 Subject: [PATCH 04/26] Add files via upload --- src/api/auth.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/api/auth.ts b/src/api/auth.ts index 8a1b163..878b577 100644 --- a/src/api/auth.ts +++ b/src/api/auth.ts @@ -42,6 +42,17 @@ export const authApi = { return response.data }, + // Register standalone email account (no Telegram required) + registerEmailStandalone: async (data: { + email: string + password: string + first_name?: string + language?: string + }): Promise => { + const response = await apiClient.post('/cabinet/auth/email/register/standalone', data) + return response.data + }, + // Verify email verifyEmail: async (token: string): Promise<{ message: string }> => { const response = await apiClient.post('/cabinet/auth/email/verify', { token }) From de76ae5e02a76d99ecb57235d39b36e3645ae8fa Mon Sep 17 00:00:00 2001 From: Egor Date: Fri, 23 Jan 2026 11:34:34 +0300 Subject: [PATCH 05/26] Update index.ts --- src/types/index.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/types/index.ts b/src/types/index.ts index ebebcba..336079f 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -1,7 +1,7 @@ // User types export interface User { id: number - telegram_id: number + telegram_id: number | null // Nullable для email-only пользователей username: string | null first_name: string | null last_name: string | null @@ -12,6 +12,7 @@ export interface User { referral_code: string | null language: string created_at: string + auth_type: 'telegram' | 'email' // Тип аутентификации } // Auth types From c24fbbed3a1bbe341341bcebf05823fb59e03bc0 Mon Sep 17 00:00:00 2001 From: Egor Date: Fri, 23 Jan 2026 11:34:58 +0300 Subject: [PATCH 06/26] Add files via upload From 012fced2a1c99c591af0fba2bd531242a261f0a4 Mon Sep 17 00:00:00 2001 From: Egor Date: Fri, 23 Jan 2026 14:54:30 +0300 Subject: [PATCH 07/26] Update TopUpModal.tsx --- src/components/TopUpModal.tsx | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/src/components/TopUpModal.tsx b/src/components/TopUpModal.tsx index 4086f0c..3a95d5b 100644 --- a/src/components/TopUpModal.tsx +++ b/src/components/TopUpModal.tsx @@ -202,18 +202,8 @@ export default function TopUpModal({ method, onClose, initialAmountRubles }: Top onSuccess: (data) => { const redirectUrl = data.payment_url || (data as any).invoice_url if (redirectUrl) { - // In Telegram Mini App, try to open directly - const webApp = window.Telegram?.WebApp - if (webApp?.openLink) { - try { - webApp.openLink(redirectUrl, { try_instant_view: false, try_browser: true }) - onClose() - return - } catch (e) { - console.warn('[TopUpModal] webApp.openLink failed:', e) - } - } - // Otherwise show the link for user to click + // Always show the payment link for user to click manually + // This ensures it works on all platforms including iOS Safari setPaymentUrl(redirectUrl) } }, From fe5be1f08d7abcad078c7c957d1c725b21de206e Mon Sep 17 00:00:00 2001 From: Egor Date: Sun, 25 Jan 2026 08:44:25 +0300 Subject: [PATCH 08/26] Add files via upload --- src/pages/Login.tsx | 29 ++++++++-- src/pages/Profile.tsx | 125 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 150 insertions(+), 4 deletions(-) diff --git a/src/pages/Login.tsx b/src/pages/Login.tsx index 90e7cdc..0bcfce7 100644 --- a/src/pages/Login.tsx +++ b/src/pages/Login.tsx @@ -1,5 +1,5 @@ import { useState, useEffect, useMemo, useCallback } from 'react' -import { useNavigate, useLocation } from 'react-router-dom' +import { useNavigate, useLocation, useSearchParams } from 'react-router-dom' import { useTranslation } from 'react-i18next' import { useQuery } from '@tanstack/react-query' import { useAuthStore } from '../store/auth' @@ -12,9 +12,18 @@ export default function Login() { const { t } = useTranslation() const navigate = useNavigate() const location = useLocation() + const [searchParams] = useSearchParams() const { isAuthenticated, loginWithTelegram, loginWithEmail, registerWithEmail } = useAuthStore() - const [activeTab, setActiveTab] = useState<'telegram' | 'email'>('telegram') - const [authMode, setAuthMode] = useState<'login' | 'register'>('login') + + // Extract referral code from URL + const referralCode = searchParams.get('ref') || '' + + const [activeTab, setActiveTab] = useState<'telegram' | 'email'>(() => + referralCode ? 'email' : 'telegram' + ) + const [authMode, setAuthMode] = useState<'login' | 'register'>(() => + referralCode ? 'register' : 'login' + ) const [email, setEmail] = useState('') const [password, setPassword] = useState('') const [confirmPassword, setConfirmPassword] = useState('') @@ -126,7 +135,7 @@ export default function Login() { if (authMode === 'login') { await loginWithEmail(email, password) } else { - await registerWithEmail(email, password, firstName || undefined) + await registerWithEmail(email, password, firstName || undefined, referralCode || undefined) } navigate(getReturnUrl(), { replace: true }) } catch (err: unknown) { @@ -185,6 +194,18 @@ export default function Login() {

{t('auth.loginSubtitle')}

+ + {/* Referral Banner */} + {referralCode && ( +
+
+ + + + {t('auth.referralInvite')} +
+
+ )} {/* Card */} diff --git a/src/pages/Profile.tsx b/src/pages/Profile.tsx index ddc9079..5eba475 100644 --- a/src/pages/Profile.tsx +++ b/src/pages/Profile.tsx @@ -1,9 +1,38 @@ import { useState } from 'react' +import { Link } from 'react-router-dom' import { useTranslation } from 'react-i18next' import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' import { useAuthStore } from '../store/auth' import { authApi } from '../api/auth' import { notificationsApi, NotificationSettings, NotificationSettingsUpdate } from '../api/notifications' +import { referralApi } from '../api/referral' +import { brandingApi } from '../api/branding' + +// Icons +const CopyIcon = () => ( + + + +) + +const CheckIcon = () => ( + + + +) + +const ShareIcon = () => ( + + + + +) + +const ArrowRightIcon = () => ( + + + +) export default function Profile() { const { t } = useTranslation() @@ -15,6 +44,58 @@ export default function Profile() { const [confirmPassword, setConfirmPassword] = useState('') const [error, setError] = useState(null) const [success, setSuccess] = useState(null) + const [copied, setCopied] = useState(false) + + // Referral data + const { data: referralInfo } = useQuery({ + queryKey: ['referral-info'], + queryFn: referralApi.getReferralInfo, + }) + + const { data: referralTerms } = useQuery({ + queryKey: ['referral-terms'], + queryFn: referralApi.getReferralTerms, + }) + + const { data: branding } = useQuery({ + queryKey: ['branding'], + queryFn: brandingApi.getBranding, + staleTime: 60000, + }) + + // Build referral link + const botUsername = import.meta.env.VITE_TELEGRAM_BOT_USERNAME + const referralLink = referralInfo?.referral_code && botUsername + ? `https://t.me/${botUsername}?start=${referralInfo.referral_code}` + : referralInfo?.referral_link || '' + + const copyReferralLink = () => { + if (referralLink) { + navigator.clipboard.writeText(referralLink) + setCopied(true) + setTimeout(() => setCopied(false), 2000) + } + } + + const shareReferralLink = () => { + if (!referralLink) return + const shareText = t('referral.shareMessage', { + percent: referralInfo?.commission_percent || 0, + botName: branding?.name || import.meta.env.VITE_APP_NAME || 'Cabinet', + }) + + if (navigator.share) { + navigator.share({ + title: t('referral.title'), + text: shareText, + url: referralLink, + }).catch(() => {}) + return + } + + const telegramUrl = `https://t.me/share/url?url=${encodeURIComponent(referralLink)}&text=${encodeURIComponent(shareText)}` + window.open(telegramUrl, '_blank', 'noopener,noreferrer') + } const registerEmailMutation = useMutation({ mutationFn: ({ email, password }: { email: string; password: string }) => @@ -127,6 +208,50 @@ export default function Profile() { + {/* Referral Link Widget */} + {referralTerms?.is_enabled && referralLink && ( +
+
+

{t('referral.yourLink')}

+ + {t('referral.title')} + + +
+
+
+ +
+
+ + +
+
+

+ {t('referral.shareHint', { percent: referralInfo?.commission_percent || 0 })} +

+
+ )} + {/* Email Section */}

{t('profile.emailAuth')}

From 8ea747c9274f3144283e5f896b37bbd581d1e8ae Mon Sep 17 00:00:00 2001 From: Egor Date: Sun, 25 Jan 2026 08:45:39 +0300 Subject: [PATCH 09/26] Update auth.ts --- src/store/auth.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/store/auth.ts b/src/store/auth.ts index ccee53d..3f6979c 100644 --- a/src/store/auth.ts +++ b/src/store/auth.ts @@ -33,7 +33,7 @@ interface AuthState { loginWithTelegram: (initData: string) => Promise loginWithTelegramWidget: (data: TelegramWidgetData) => Promise loginWithEmail: (email: string, password: string) => Promise - registerWithEmail: (email: string, password: string, firstName?: string) => Promise + registerWithEmail: (email: string, password: string, firstName?: string, referralCode?: string) => Promise } // Блокировка для предотвращения race condition при инициализации @@ -262,12 +262,13 @@ export const useAuthStore = create()( get().checkAdminStatus() }, - registerWithEmail: async (email, password, firstName) => { + registerWithEmail: async (email, password, firstName, referralCode) => { const response = await authApi.registerEmailStandalone({ email, password, first_name: firstName, language: navigator.language.split('-')[0] || 'ru', + referral_code: referralCode, }) tokenStorage.setTokens(response.access_token, response.refresh_token) set({ From 9688fc55818296cf68e5aae77ea0397c2621bb5f Mon Sep 17 00:00:00 2001 From: Egor Date: Sun, 25 Jan 2026 08:46:14 +0300 Subject: [PATCH 10/26] Update auth.ts --- src/api/auth.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/api/auth.ts b/src/api/auth.ts index 878b577..3efb12f 100644 --- a/src/api/auth.ts +++ b/src/api/auth.ts @@ -48,6 +48,7 @@ export const authApi = { password: string first_name?: string language?: string + referral_code?: string }): Promise => { const response = await apiClient.post('/cabinet/auth/email/register/standalone', data) return response.data From e866eb237f649cd826d5c02f6c616376aa17dd07 Mon Sep 17 00:00:00 2001 From: Egor Date: Sun, 25 Jan 2026 08:47:09 +0300 Subject: [PATCH 11/26] Add files via upload --- src/locales/en.json | 1 + src/locales/fa.json | 3 ++- src/locales/ru.json | 1 + src/locales/zh.json | 3 ++- 4 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/locales/en.json b/src/locales/en.json index 77f3704..100e6f0 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -79,6 +79,7 @@ "welcomeBack": "Welcome back!", "loginTitle": "Login to Cabinet", "loginSubtitle": "Sign in with Telegram or use your email", + "referralInvite": "You've been invited via referral link!", "passwordMismatch": "Passwords do not match", "passwordTooShort": "Password must be at least 8 characters", "invalidEmail": "Please enter a valid email address", diff --git a/src/locales/fa.json b/src/locales/fa.json index b03b8b6..e159913 100644 --- a/src/locales/fa.json +++ b/src/locales/fa.json @@ -73,7 +73,8 @@ "tryAgain": "تلاش مجدد", "welcomeBack": "خوش آمدید!", "loginTitle": "ورود به کابین", - "loginSubtitle": "با تلگرام یا ایمیل وارد شوید" + "loginSubtitle": "با تلگرام یا ایمیل وارد شوید", + "referralInvite": "شما از طریق لینک معرفی دعوت شده‌اید!" }, "emailVerification": { "title": "تایید ایمیل", diff --git a/src/locales/ru.json b/src/locales/ru.json index 5ac0cda..109ccf4 100644 --- a/src/locales/ru.json +++ b/src/locales/ru.json @@ -79,6 +79,7 @@ "welcomeBack": "Добро пожаловать!", "loginTitle": "Вход в личный кабинет", "loginSubtitle": "Войдите через Telegram или используйте email", + "referralInvite": "Вы приглашены по реферальной ссылке!", "passwordMismatch": "Пароли не совпадают", "passwordTooShort": "Пароль должен быть минимум 8 символов", "invalidEmail": "Введите корректный email адрес", diff --git a/src/locales/zh.json b/src/locales/zh.json index 42a892b..fbf2f76 100644 --- a/src/locales/zh.json +++ b/src/locales/zh.json @@ -74,7 +74,8 @@ "tryAgain": "重试", "welcomeBack": "欢迎回来!", "loginTitle": "登录个人中心", - "loginSubtitle": "通过Telegram或邮箱登录" + "loginSubtitle": "通过Telegram或邮箱登录", + "referralInvite": "您已通过推荐链接受邀!" }, "emailVerification": { "title": "邮箱验证", From 1b5e6167004072fb40d5ec10fa2c6517361075f2 Mon Sep 17 00:00:00 2001 From: Egor Date: Sun, 25 Jan 2026 08:53:29 +0300 Subject: [PATCH 12/26] Add files via upload --- src/locales/en.json | 2 ++ src/locales/fa.json | 2 ++ src/locales/ru.json | 2 ++ src/locales/zh.json | 2 ++ 4 files changed, 8 insertions(+) diff --git a/src/locales/en.json b/src/locales/en.json index 100e6f0..44e39ef 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -1210,6 +1210,8 @@ "resendVerification": "Resend Verification Email", "verificationResent": "Verification email resent!", "canLoginWithEmail": "You can now log in using your email and password.", + "emailAlreadyRegistered": "This email is already linked to another account. If this is your email, log in with it or contact support.", + "alreadyHaveEmail": "You already have a verified email linked.", "notifications": { "title": "Notification Settings", "subscriptionExpiry": "Subscription Expiry", diff --git a/src/locales/fa.json b/src/locales/fa.json index e159913..97063a7 100644 --- a/src/locales/fa.json +++ b/src/locales/fa.json @@ -683,6 +683,8 @@ "resendVerification": "ارسال مجدد ایمیل تایید", "verificationResent": "ایمیل تایید مجدداً ارسال شد!", "canLoginWithEmail": "اکنون می‌توانید با ایمیل و رمز عبور وارد شوید.", + "emailAlreadyRegistered": "این ایمیل قبلاً به حساب دیگری متصل شده است. اگر این ایمیل شماست، با آن وارد شوید یا با پشتیبانی تماس بگیرید.", + "alreadyHaveEmail": "شما قبلاً یک ایمیل تایید شده متصل کرده‌اید.", "notifications": { "title": "تنظیمات اعلان", "subscriptionExpiry": "انقضای اشتراک", diff --git a/src/locales/ru.json b/src/locales/ru.json index 109ccf4..4faa8ed 100644 --- a/src/locales/ru.json +++ b/src/locales/ru.json @@ -1903,6 +1903,8 @@ "resendVerification": "Отправить письмо повторно", "verificationResent": "Письмо отправлено повторно!", "canLoginWithEmail": "Теперь вы можете входить с помощью email и пароля.", + "emailAlreadyRegistered": "Этот email уже привязан к другому аккаунту. Если это ваш email, войдите через него или обратитесь в поддержку.", + "alreadyHaveEmail": "У вас уже привязан подтверждённый email.", "notifications": { "title": "Настройки уведомлений", "subscriptionExpiry": "Окончание подписки", diff --git a/src/locales/zh.json b/src/locales/zh.json index fbf2f76..3521cce 100644 --- a/src/locales/zh.json +++ b/src/locales/zh.json @@ -684,6 +684,8 @@ "resendVerification": "重新发送验证邮件", "verificationResent": "验证邮件已重新发送!", "canLoginWithEmail": "现在您可以使用邮箱和密码登录。", + "emailAlreadyRegistered": "此邮箱已绑定到其他账户。如果这是您的邮箱,请使用邮箱登录或联系客服。", + "alreadyHaveEmail": "您已经绑定了已验证的邮箱。", "notifications": { "title": "通知设置", "subscriptionExpiry": "订阅到期", From 97b2fa319a88e4ad6fb4a02047b5d7a350cd986d Mon Sep 17 00:00:00 2001 From: Egor Date: Sun, 25 Jan 2026 08:54:19 +0300 Subject: [PATCH 13/26] Update Profile.tsx --- src/pages/Profile.tsx | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/pages/Profile.tsx b/src/pages/Profile.tsx index 5eba475..4bb14f6 100644 --- a/src/pages/Profile.tsx +++ b/src/pages/Profile.tsx @@ -111,7 +111,14 @@ export default function Profile() { queryClient.invalidateQueries({ queryKey: ['user'] }) }, onError: (err: { response?: { data?: { detail?: string } } }) => { - setError(err.response?.data?.detail || t('common.error')) + const detail = err.response?.data?.detail + if (detail?.includes('already registered')) { + setError(t('profile.emailAlreadyRegistered')) + } else if (detail?.includes('already have a verified email')) { + setError(t('profile.alreadyHaveEmail')) + } else { + setError(detail || t('common.error')) + } setSuccess(null) }, }) From 13c7d234226622fc3e016907fab1cef54b489c4e Mon Sep 17 00:00:00 2001 From: Egor Date: Sun, 25 Jan 2026 09:05:28 +0300 Subject: [PATCH 14/26] Add files via upload --- src/pages/Profile.tsx | 9 ++++----- src/pages/Referral.tsx | 9 ++++----- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/src/pages/Profile.tsx b/src/pages/Profile.tsx index 4bb14f6..1916490 100644 --- a/src/pages/Profile.tsx +++ b/src/pages/Profile.tsx @@ -63,11 +63,10 @@ export default function Profile() { staleTime: 60000, }) - // Build referral link - const botUsername = import.meta.env.VITE_TELEGRAM_BOT_USERNAME - const referralLink = referralInfo?.referral_code && botUsername - ? `https://t.me/${botUsername}?start=${referralInfo.referral_code}` - : referralInfo?.referral_link || '' + // Build referral link for cabinet + const referralLink = referralInfo?.referral_code + ? `${window.location.origin}/login?ref=${referralInfo.referral_code}` + : '' const copyReferralLink = () => { if (referralLink) { diff --git a/src/pages/Referral.tsx b/src/pages/Referral.tsx index 3febac8..c1e01c8 100644 --- a/src/pages/Referral.tsx +++ b/src/pages/Referral.tsx @@ -68,11 +68,10 @@ export default function Referral() { queryFn: referralApi.getReferralInfo, }) - // Build referral link using frontend env variable for correct bot username - const botUsername = import.meta.env.VITE_TELEGRAM_BOT_USERNAME - const referralLink = info?.referral_code && botUsername - ? `https://t.me/${botUsername}?start=${info.referral_code}` - : info?.referral_link || '' + // Build referral link for cabinet registration + const referralLink = info?.referral_code + ? `${window.location.origin}/login?ref=${info.referral_code}` + : '' const { data: terms } = useQuery({ queryKey: ['referral-terms'], From 3f9c687f37ceb0af1ba77a542bd1a9bb6519ef87 Mon Sep 17 00:00:00 2001 From: Egor Date: Sun, 25 Jan 2026 09:24:54 +0300 Subject: [PATCH 15/26] Update index.ts --- src/types/index.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/types/index.ts b/src/types/index.ts index 336079f..a93e836 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -31,6 +31,12 @@ export interface TokenResponse { expires_in: number } +export interface RegisterResponse { + message: string + email: string + requires_verification: boolean +} + // Subscription types export interface ServerInfo { uuid: string From cb1476f8083a7f116e9a0ec8d6d2966d6122900d Mon Sep 17 00:00:00 2001 From: Egor Date: Sun, 25 Jan 2026 09:25:27 +0300 Subject: [PATCH 16/26] Update auth.ts --- src/api/auth.ts | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/api/auth.ts b/src/api/auth.ts index 3efb12f..a5697e2 100644 --- a/src/api/auth.ts +++ b/src/api/auth.ts @@ -1,5 +1,5 @@ import apiClient from './client' -import type { AuthResponse, TokenResponse, User } from '../types' +import type { AuthResponse, RegisterResponse, TokenResponse, User } from '../types' export const authApi = { // Telegram WebApp authentication @@ -43,20 +43,21 @@ export const authApi = { }, // Register standalone email account (no Telegram required) + // Returns message - user must verify email before login registerEmailStandalone: async (data: { email: string password: string first_name?: string language?: string referral_code?: string - }): Promise => { - const response = await apiClient.post('/cabinet/auth/email/register/standalone', data) + }): Promise => { + const response = await apiClient.post('/cabinet/auth/email/register/standalone', data) return response.data }, - // Verify email - verifyEmail: async (token: string): Promise<{ message: string }> => { - const response = await apiClient.post('/cabinet/auth/email/verify', { token }) + // Verify email and get auth tokens + verifyEmail: async (token: string): Promise => { + const response = await apiClient.post('/cabinet/auth/email/verify', { token }) return response.data }, From 2519fdcee28d381fb5bc8aa94179b34b929737dc Mon Sep 17 00:00:00 2001 From: Egor Date: Sun, 25 Jan 2026 09:25:56 +0300 Subject: [PATCH 17/26] Update auth.ts --- src/store/auth.ts | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/src/store/auth.ts b/src/store/auth.ts index 3f6979c..f62dc2f 100644 --- a/src/store/auth.ts +++ b/src/store/auth.ts @@ -1,6 +1,6 @@ import { create } from 'zustand' import { persist } from 'zustand/middleware' -import type { User } from '../types' +import type { RegisterResponse, User } from '../types' import { authApi } from '../api/auth' import { apiClient } from '../api/client' import { tokenStorage, isTokenValid, tokenRefreshManager } from '../utils/token' @@ -33,7 +33,7 @@ interface AuthState { loginWithTelegram: (initData: string) => Promise loginWithTelegramWidget: (data: TelegramWidgetData) => Promise loginWithEmail: (email: string, password: string) => Promise - registerWithEmail: (email: string, password: string, firstName?: string, referralCode?: string) => Promise + registerWithEmail: (email: string, password: string, firstName?: string, referralCode?: string) => Promise } // Блокировка для предотвращения race condition при инициализации @@ -263,6 +263,8 @@ export const useAuthStore = create()( }, registerWithEmail: async (email, password, firstName, referralCode) => { + // Registration now returns message, not tokens + // User must verify email before they can login const response = await authApi.registerEmailStandalone({ email, password, @@ -270,14 +272,7 @@ export const useAuthStore = create()( language: navigator.language.split('-')[0] || 'ru', referral_code: referralCode, }) - tokenStorage.setTokens(response.access_token, response.refresh_token) - set({ - accessToken: response.access_token, - refreshToken: response.refresh_token, - user: response.user, - isAuthenticated: true, - }) - get().checkAdminStatus() + return response }, }), { From d00bb5463bb79d14bce6629c8a954f127b34ba41 Mon Sep 17 00:00:00 2001 From: Egor Date: Sun, 25 Jan 2026 09:26:24 +0300 Subject: [PATCH 18/26] Add files via upload --- src/pages/Login.tsx | 47 ++++++++++++++++++++++++++++++++++----- src/pages/VerifyEmail.tsx | 25 ++++++++++++++------- 2 files changed, 59 insertions(+), 13 deletions(-) diff --git a/src/pages/Login.tsx b/src/pages/Login.tsx index 0bcfce7..9ba4b78 100644 --- a/src/pages/Login.tsx +++ b/src/pages/Login.tsx @@ -32,6 +32,7 @@ export default function Login() { const [isLoading, setIsLoading] = useState(false) const [isTelegramWebApp, setIsTelegramWebApp] = useState(false) const [logoLoaded, setLogoLoaded] = useState(() => isLogoPreloaded()) + const [registeredEmail, setRegisteredEmail] = useState(null) // Получаем URL для возврата после авторизации const getReturnUrl = useCallback(() => { @@ -134,10 +135,12 @@ export default function Login() { try { if (authMode === 'login') { await loginWithEmail(email, password) + navigate(getReturnUrl(), { replace: true }) } else { - await registerWithEmail(email, password, firstName || undefined, referralCode || undefined) + const result = await registerWithEmail(email, password, firstName || undefined, referralCode || undefined) + // Show "check your email" screen + setRegisteredEmail(result.email) } - navigate(getReturnUrl(), { replace: true }) } catch (err: unknown) { const error = err as { response?: { status?: number; data?: { detail?: string } } } const status = error.response?.status @@ -146,11 +149,15 @@ export default function Login() { if (status === 400 && detail?.includes('already registered')) { setError(t('auth.emailAlreadyRegistered', 'This email is already registered')) } else if (status === 401 || status === 403) { - setError(t('auth.invalidCredentials', 'Invalid email or password')) + if (detail?.includes('verify your email')) { + setError(t('auth.emailNotVerified', 'Please verify your email first')) + } else { + setError(t('auth.invalidCredentials', 'Invalid email or password')) + } } else if (status === 429) { setError(t('auth.tooManyAttempts', 'Too many attempts. Please try again later')) } else { - setError(t('common.error')) + setError(detail || t('common.error')) } } finally { setIsLoading(false) @@ -208,7 +215,36 @@ export default function Login() { )}
- {/* Card */} + {/* Check Email Screen */} + {registeredEmail ? ( +
+
+ + + +
+

+ {t('auth.checkEmail', 'Check your email')} +

+

+ {t('auth.verificationSent', 'We sent a verification link to:')} +

+

{registeredEmail}

+

+ {t('auth.clickLinkToVerify', 'Click the link in the email to verify your account and log in.')} +

+ +
+ ) : ( + /* Card */
{/* Tabs */}
@@ -389,6 +425,7 @@ export default function Login() {
)}
+ )} ) diff --git a/src/pages/VerifyEmail.tsx b/src/pages/VerifyEmail.tsx index ee670f6..b44eda0 100644 --- a/src/pages/VerifyEmail.tsx +++ b/src/pages/VerifyEmail.tsx @@ -1,14 +1,18 @@ import { useEffect, useState } from 'react' -import { useSearchParams, Link } from 'react-router-dom' +import { useSearchParams, Link, useNavigate } from 'react-router-dom' import { useTranslation } from 'react-i18next' import { authApi } from '../api/auth' +import { useAuthStore } from '../store/auth' +import { tokenStorage } from '../utils/token' import LanguageSwitcher from '../components/LanguageSwitcher' export default function VerifyEmail() { const { t } = useTranslation() + const navigate = useNavigate() const [searchParams] = useSearchParams() const [status, setStatus] = useState<'loading' | 'success' | 'error'>('loading') const [error, setError] = useState('') + const { setTokens, setUser, checkAdminStatus } = useAuthStore() useEffect(() => { const token = searchParams.get('token') @@ -21,8 +25,15 @@ export default function VerifyEmail() { const verify = async () => { try { - await authApi.verifyEmail(token) + const response = await authApi.verifyEmail(token) + // Save tokens and log user in + tokenStorage.setTokens(response.access_token, response.refresh_token) + setTokens(response.access_token, response.refresh_token) + setUser(response.user) + checkAdminStatus() setStatus('success') + // Redirect to dashboard after short delay + setTimeout(() => navigate('/', { replace: true }), 1500) } catch (err: unknown) { setStatus('error') const error = err as { response?: { data?: { detail?: string } } } @@ -31,7 +42,7 @@ export default function VerifyEmail() { } verify() - }, [searchParams, t]) + }, [searchParams, t, navigate, setTokens, setUser, checkAdminStatus]) return (
@@ -54,12 +65,10 @@ export default function VerifyEmail() {

{t('emailVerification.success')}

- {t('emailVerification.successMessage')} + {t('emailVerification.redirecting', 'Redirecting to dashboard...')}

-
- - {t('emailVerification.goToLogin')} - +
+
)} From 7169988d2013a57e676833763af34ee83a2cbe88 Mon Sep 17 00:00:00 2001 From: Egor Date: Sun, 25 Jan 2026 09:26:48 +0300 Subject: [PATCH 19/26] Add files via upload --- src/locales/en.json | 8 +++++++- src/locales/fa.json | 8 +++++++- src/locales/ru.json | 8 +++++++- src/locales/zh.json | 8 +++++++- 4 files changed, 28 insertions(+), 4 deletions(-) diff --git a/src/locales/en.json b/src/locales/en.json index 44e39ef..3a25fe2 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -86,7 +86,12 @@ "emailAlreadyRegistered": "This email is already registered", "invalidCredentials": "Invalid email or password", "tooManyAttempts": "Too many attempts. Please try again later", - "verificationEmailNotice": "After registration, a verification email will be sent to your address" + "verificationEmailNotice": "After registration, a verification email will be sent to your address", + "checkEmail": "Check your email", + "verificationSent": "We sent a verification link to:", + "clickLinkToVerify": "Click the link in the email to verify your account and log in.", + "backToLogin": "Back to login", + "emailNotVerified": "Please verify your email first" }, "emailVerification": { "title": "Email Verification", @@ -94,6 +99,7 @@ "pleaseWait": "Please wait while we verify your email address.", "success": "Email Verified!", "successMessage": "Your email has been successfully verified. You can now log in with your email and password.", + "redirecting": "Redirecting to dashboard...", "failed": "Verification Failed", "goToLogin": "Go to Login" }, diff --git a/src/locales/fa.json b/src/locales/fa.json index 97063a7..cc418bc 100644 --- a/src/locales/fa.json +++ b/src/locales/fa.json @@ -74,7 +74,12 @@ "welcomeBack": "خوش آمدید!", "loginTitle": "ورود به کابین", "loginSubtitle": "با تلگرام یا ایمیل وارد شوید", - "referralInvite": "شما از طریق لینک معرفی دعوت شده‌اید!" + "referralInvite": "شما از طریق لینک معرفی دعوت شده‌اید!", + "checkEmail": "ایمیل خود را بررسی کنید", + "verificationSent": "لینک تایید به این آدرس ارسال شد:", + "clickLinkToVerify": "روی لینک موجود در ایمیل کلیک کنید تا حساب خود را تایید و وارد شوید.", + "backToLogin": "بازگشت به ورود", + "emailNotVerified": "لطفاً ابتدا ایمیل خود را تایید کنید" }, "emailVerification": { "title": "تایید ایمیل", @@ -82,6 +87,7 @@ "pleaseWait": "لطفاً صبر کنید تا ایمیل شما تایید شود.", "success": "ایمیل تایید شد!", "successMessage": "ایمیل شما با موفقیت تایید شد. اکنون می‌توانید با ایمیل و رمز عبور وارد شوید.", + "redirecting": "در حال انتقال به داشبورد...", "failed": "تایید ناموفق", "goToLogin": "رفتن به صفحه ورود" }, diff --git a/src/locales/ru.json b/src/locales/ru.json index 4faa8ed..0e1a0c4 100644 --- a/src/locales/ru.json +++ b/src/locales/ru.json @@ -86,7 +86,12 @@ "emailAlreadyRegistered": "Этот email уже зарегистрирован", "invalidCredentials": "Неверный email или пароль", "tooManyAttempts": "Слишком много попыток. Попробуйте позже", - "verificationEmailNotice": "После регистрации на вашу почту будет отправлено письмо для подтверждения" + "verificationEmailNotice": "После регистрации на вашу почту будет отправлено письмо для подтверждения", + "checkEmail": "Проверьте почту", + "verificationSent": "Мы отправили ссылку для подтверждения на:", + "clickLinkToVerify": "Перейдите по ссылке в письме, чтобы подтвердить аккаунт и войти.", + "backToLogin": "Вернуться ко входу", + "emailNotVerified": "Сначала подтвердите email" }, "emailVerification": { "title": "Подтверждение email", @@ -94,6 +99,7 @@ "pleaseWait": "Пожалуйста, подождите пока мы проверяем ваш email.", "success": "Email подтверждён!", "successMessage": "Ваш email успешно подтверждён. Теперь вы можете входить с помощью email и пароля.", + "redirecting": "Переходим в личный кабинет...", "failed": "Ошибка подтверждения", "goToLogin": "Перейти к входу" }, diff --git a/src/locales/zh.json b/src/locales/zh.json index 3521cce..2f26e61 100644 --- a/src/locales/zh.json +++ b/src/locales/zh.json @@ -75,7 +75,12 @@ "welcomeBack": "欢迎回来!", "loginTitle": "登录个人中心", "loginSubtitle": "通过Telegram或邮箱登录", - "referralInvite": "您已通过推荐链接受邀!" + "referralInvite": "您已通过推荐链接受邀!", + "checkEmail": "请查收邮件", + "verificationSent": "我们已发送验证链接至:", + "clickLinkToVerify": "点击邮件中的链接验证账户并登录。", + "backToLogin": "返回登录", + "emailNotVerified": "请先验证邮箱" }, "emailVerification": { "title": "邮箱验证", @@ -83,6 +88,7 @@ "pleaseWait": "请稍候,我们正在验证您的邮箱。", "success": "邮箱已验证!", "successMessage": "您的邮箱已成功验证。现在您可以使用邮箱和密码登录。", + "redirecting": "正在跳转到个人中心...", "failed": "验证失败", "goToLogin": "前往登录" }, From c358d8caabeb14135ab0e882babb0c9a18a0a26e Mon Sep 17 00:00:00 2001 From: Egor Date: Sun, 25 Jan 2026 09:45:16 +0300 Subject: [PATCH 20/26] Add files via upload --- src/pages/Login.tsx | 80 +++++++++++++++++++++++++++++-------------- src/pages/Profile.tsx | 14 ++++++-- 2 files changed, 66 insertions(+), 28 deletions(-) diff --git a/src/pages/Login.tsx b/src/pages/Login.tsx index 9ba4b78..45f5182 100644 --- a/src/pages/Login.tsx +++ b/src/pages/Login.tsx @@ -3,7 +3,7 @@ import { useNavigate, useLocation, useSearchParams } from 'react-router-dom' import { useTranslation } from 'react-i18next' import { useQuery } from '@tanstack/react-query' import { useAuthStore } from '../store/auth' -import { brandingApi, getCachedBranding, setCachedBranding, preloadLogo, isLogoPreloaded, type BrandingInfo } from '../api/branding' +import { brandingApi, getCachedBranding, setCachedBranding, preloadLogo, isLogoPreloaded, type BrandingInfo, type EmailAuthEnabled } from '../api/branding' import { getAndClearReturnUrl } from '../utils/token' import LanguageSwitcher from '../components/LanguageSwitcher' import TelegramLoginButton from '../components/TelegramLoginButton' @@ -65,7 +65,29 @@ export default function Login() { initialData: cachedBranding ?? undefined, }) + // Check if email auth is enabled + const { data: emailAuthConfig } = useQuery({ + queryKey: ['email-auth-enabled'], + queryFn: brandingApi.getEmailAuthEnabled, + staleTime: 60000, + }) + const isEmailAuthEnabled = emailAuthConfig?.enabled ?? true + const botUsername = import.meta.env.VITE_TELEGRAM_BOT_USERNAME || '' + + // If email auth is disabled but user came with ref param, redirect to bot + useEffect(() => { + if (referralCode && emailAuthConfig?.enabled === false && botUsername) { + window.location.href = `https://t.me/${botUsername}?start=ref_${referralCode}` + } + }, [referralCode, emailAuthConfig, botUsername]) + + // If email auth is disabled but we initially set to email tab, switch back to telegram + useEffect(() => { + if (!isEmailAuthEnabled && activeTab === 'email') { + setActiveTab('telegram') + } + }, [isEmailAuthEnabled, activeTab]) const appName = branding ? branding.name : (import.meta.env.VITE_APP_NAME || 'VPN') const appLogo = branding?.logo_letter || import.meta.env.VITE_APP_LOGO || 'V' const logoUrl = branding ? brandingApi.getLogoUrl(branding) : null @@ -202,8 +224,8 @@ export default function Login() { {t('auth.loginSubtitle')}

- {/* Referral Banner */} - {referralCode && ( + {/* Referral Banner - only show when email auth is enabled */} + {referralCode && isEmailAuthEnabled && (
@@ -247,28 +269,34 @@ export default function Login() { /* Card */
{/* Tabs */} -
- - -
+ {isEmailAuthEnabled ? ( +
+ + +
+ ) : ( +
+

Telegram

+
+ )} {error && (
@@ -276,7 +304,7 @@ export default function Login() {
)} - {activeTab === 'telegram' ? ( + {activeTab === 'telegram' || !isEmailAuthEnabled ? (

{t('auth.registerHint')} diff --git a/src/pages/Profile.tsx b/src/pages/Profile.tsx index 1916490..fb2aa67 100644 --- a/src/pages/Profile.tsx +++ b/src/pages/Profile.tsx @@ -6,7 +6,7 @@ import { useAuthStore } from '../store/auth' import { authApi } from '../api/auth' import { notificationsApi, NotificationSettings, NotificationSettingsUpdate } from '../api/notifications' import { referralApi } from '../api/referral' -import { brandingApi } from '../api/branding' +import { brandingApi, type EmailAuthEnabled } from '../api/branding' // Icons const CopyIcon = () => ( @@ -63,6 +63,14 @@ export default function Profile() { staleTime: 60000, }) + // Check if email auth is enabled + const { data: emailAuthConfig } = useQuery({ + queryKey: ['email-auth-enabled'], + queryFn: brandingApi.getEmailAuthEnabled, + staleTime: 60000, + }) + const isEmailAuthEnabled = emailAuthConfig?.enabled ?? true + // Build referral link for cabinet const referralLink = referralInfo?.referral_code ? `${window.location.origin}/login?ref=${referralInfo.referral_code}` @@ -258,7 +266,8 @@ export default function Profile() {

)} - {/* Email Section */} + {/* Email Section - only show when email auth is enabled */} + {isEmailAuthEnabled && (

{t('profile.emailAuth')}

@@ -381,6 +390,7 @@ export default function Profile() {
)}
+ )} {/* Notification Settings */}
From 1c3b29aa60c66159b6dda776fa7d0cb494cd5f18 Mon Sep 17 00:00:00 2001 From: Egor Date: Sun, 25 Jan 2026 09:45:41 +0300 Subject: [PATCH 21/26] Add files via upload --- src/locales/ru.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/locales/ru.json b/src/locales/ru.json index 0e1a0c4..8a3ecd2 100644 --- a/src/locales/ru.json +++ b/src/locales/ru.json @@ -709,6 +709,8 @@ "animatedBackgroundDesc": "Волны на фоне приложения", "autoFullscreen": "Авто-Fullscreen", "autoFullscreenDesc": "В Telegram WebApp", + "emailAuth": "Email авторизация", + "emailAuthDesc": "Регистрация и вход через email", "availableThemes": "Доступные темы", "darkTheme": "Тёмная", "lightTheme": "Светлая", From 29af2be21c813dece621495cebdfad02890710e2 Mon Sep 17 00:00:00 2001 From: Egor Date: Sun, 25 Jan 2026 09:46:05 +0300 Subject: [PATCH 22/26] Update branding.ts --- src/api/branding.ts | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/src/api/branding.ts b/src/api/branding.ts index c0f6e02..d1f35e6 100644 --- a/src/api/branding.ts +++ b/src/api/branding.ts @@ -15,6 +15,10 @@ export interface FullscreenEnabled { enabled: boolean } +export interface EmailAuthEnabled { + enabled: boolean +} + const BRANDING_CACHE_KEY = 'cabinet_branding' const LOGO_PRELOADED_KEY = 'cabinet_logo_preloaded' @@ -157,4 +161,21 @@ export const brandingApi = { const response = await apiClient.patch('/cabinet/branding/fullscreen', { enabled }) return response.data }, + + // Get email auth enabled (public, no auth required) + getEmailAuthEnabled: async (): Promise => { + try { + const response = await apiClient.get('/cabinet/branding/email-auth') + return response.data + } catch { + // If endpoint doesn't exist, default to enabled + return { enabled: true } + } + }, + + // Update email auth enabled (admin only) + updateEmailAuthEnabled: async (enabled: boolean): Promise => { + const response = await apiClient.patch('/cabinet/branding/email-auth', { enabled }) + return response.data + }, } From 41fedc4fc420a6bf85e648fb630c5ff9b03e3334 Mon Sep 17 00:00:00 2001 From: Egor Date: Sun, 25 Jan 2026 10:37:14 +0300 Subject: [PATCH 23/26] Update BrandingTab.tsx --- src/components/admin/BrandingTab.tsx | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/src/components/admin/BrandingTab.tsx b/src/components/admin/BrandingTab.tsx index 82b45f1..443deac 100644 --- a/src/components/admin/BrandingTab.tsx +++ b/src/components/admin/BrandingTab.tsx @@ -35,6 +35,11 @@ export function BrandingTab({ accentColor = '#3b82f6' }: BrandingTabProps) { queryFn: brandingApi.getFullscreenEnabled, }) + const { data: emailAuthSettings } = useQuery({ + queryKey: ['email-auth-enabled'], + queryFn: brandingApi.getEmailAuthEnabled, + }) + // Mutations const updateBrandingMutation = useMutation({ mutationFn: brandingApi.updateName, @@ -77,6 +82,13 @@ export function BrandingTab({ accentColor = '#3b82f6' }: BrandingTabProps) { }, }) + const updateEmailAuthMutation = useMutation({ + mutationFn: (enabled: boolean) => brandingApi.updateEmailAuthEnabled(enabled), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['email-auth-enabled'] }) + }, + }) + const handleLogoUpload = (e: React.ChangeEvent) => { const file = e.target.files?.[0] if (file) { @@ -205,6 +217,18 @@ export function BrandingTab({ accentColor = '#3b82f6' }: BrandingTabProps) { disabled={updateFullscreenMutation.isPending} />
+ +
+
+ {t('admin.settings.emailAuth')} +

{t('admin.settings.emailAuthDesc')}

+
+ updateEmailAuthMutation.mutate(!(emailAuthSettings?.enabled ?? true))} + disabled={updateEmailAuthMutation.isPending} + /> +
From 558278b7296b6e97e52a39a16134011bc0bd8c52 Mon Sep 17 00:00:00 2001 From: Egor Date: Sun, 25 Jan 2026 10:43:36 +0300 Subject: [PATCH 24/26] Add files via upload --- src/pages/Login.tsx | 129 +++++++++++++++++++++++++- src/pages/ResetPassword.tsx | 175 ++++++++++++++++++++++++++++++++++++ 2 files changed, 301 insertions(+), 3 deletions(-) create mode 100644 src/pages/ResetPassword.tsx diff --git a/src/pages/Login.tsx b/src/pages/Login.tsx index 45f5182..194f88c 100644 --- a/src/pages/Login.tsx +++ b/src/pages/Login.tsx @@ -3,6 +3,7 @@ import { useNavigate, useLocation, useSearchParams } from 'react-router-dom' import { useTranslation } from 'react-i18next' import { useQuery } from '@tanstack/react-query' import { useAuthStore } from '../store/auth' +import { authApi } from '../api/auth' import { brandingApi, getCachedBranding, setCachedBranding, preloadLogo, isLogoPreloaded, type BrandingInfo, type EmailAuthEnabled } from '../api/branding' import { getAndClearReturnUrl } from '../utils/token' import LanguageSwitcher from '../components/LanguageSwitcher' @@ -33,6 +34,11 @@ export default function Login() { const [isTelegramWebApp, setIsTelegramWebApp] = useState(false) const [logoLoaded, setLogoLoaded] = useState(() => isLogoPreloaded()) const [registeredEmail, setRegisteredEmail] = useState(null) + const [showForgotPassword, setShowForgotPassword] = useState(false) + const [forgotPasswordEmail, setForgotPasswordEmail] = useState('') + const [forgotPasswordSent, setForgotPasswordSent] = useState(false) + const [forgotPasswordLoading, setForgotPasswordLoading] = useState(false) + const [forgotPasswordError, setForgotPasswordError] = useState('') // Получаем URL для возврата после авторизации const getReturnUrl = useCallback(() => { @@ -186,6 +192,36 @@ export default function Login() { } } + const handleForgotPassword = async (e: React.FormEvent) => { + e.preventDefault() + setForgotPasswordError('') + + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/ + if (!forgotPasswordEmail.trim() || !emailRegex.test(forgotPasswordEmail.trim())) { + setForgotPasswordError(t('auth.invalidEmail', 'Please enter a valid email address')) + return + } + + setForgotPasswordLoading(true) + try { + await authApi.forgotPassword(forgotPasswordEmail.trim()) + setForgotPasswordSent(true) + } catch (err: unknown) { + const error = err as { response?: { status?: number; data?: { detail?: string } } } + const detail = error.response?.data?.detail + setForgotPasswordError(detail || t('common.error')) + } finally { + setForgotPasswordLoading(false) + } + } + + const closeForgotPasswordModal = () => { + setShowForgotPassword(false) + setForgotPasswordEmail('') + setForgotPasswordSent(false) + setForgotPasswordError('') + } + return (
{/* Background gradient */} @@ -446,15 +482,102 @@ export default function Login() { {/* Forgot password link - only for login */} {authMode === 'login' && ( -

- {t('auth.registerHint')} -

+
+ +
)}
)} )} + + {/* Forgot Password Modal */} + {showForgotPassword && ( +
+
+
+ + + {forgotPasswordSent ? ( +
+
+ + + +
+

+ {t('auth.checkEmail', 'Check your email')} +

+

+ {t('auth.passwordResetSent', 'If an account exists with this email, we sent password reset instructions.')} +

+ +
+ ) : ( + <> +

+ {t('auth.forgotPassword', 'Forgot password?')} +

+

+ {t('auth.forgotPasswordHint', 'Enter your email and we will send you instructions to reset your password.')} +

+ +
+
+ + setForgotPasswordEmail(e.target.value)} + placeholder="you@example.com" + className="input" + autoFocus + /> +
+ + {forgotPasswordError && ( +
+ {forgotPasswordError} +
+ )} + + +
+ + )} +
+
+ )}
) } diff --git a/src/pages/ResetPassword.tsx b/src/pages/ResetPassword.tsx new file mode 100644 index 0000000..debd7f2 --- /dev/null +++ b/src/pages/ResetPassword.tsx @@ -0,0 +1,175 @@ +import { useState } from 'react' +import { useSearchParams, Link, useNavigate } from 'react-router-dom' +import { useTranslation } from 'react-i18next' +import { authApi } from '../api/auth' +import LanguageSwitcher from '../components/LanguageSwitcher' + +export default function ResetPassword() { + const { t } = useTranslation() + const navigate = useNavigate() + const [searchParams] = useSearchParams() + const token = searchParams.get('token') + + const [password, setPassword] = useState('') + const [confirmPassword, setConfirmPassword] = useState('') + const [status, setStatus] = useState<'form' | 'loading' | 'success' | 'error'>('form') + const [error, setError] = useState('') + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault() + setError('') + + if (!token) { + setError(t('resetPassword.invalidToken', 'Invalid or missing reset token')) + return + } + + if (password.length < 8) { + setError(t('auth.passwordTooShort', 'Password must be at least 8 characters')) + return + } + + if (password !== confirmPassword) { + setError(t('auth.passwordMismatch', 'Passwords do not match')) + return + } + + setStatus('loading') + + try { + await authApi.resetPassword(token, password) + setStatus('success') + setTimeout(() => navigate('/login', { replace: true }), 2000) + } catch (err: unknown) { + setStatus('error') + const error = err as { response?: { data?: { detail?: string } } } + setError(error.response?.data?.detail || t('common.error')) + } + } + + if (!token) { + return ( +
+
+
+ +
+
+
+
!
+

+ {t('resetPassword.invalidToken', 'Invalid reset link')} +

+

+ {t('resetPassword.tokenExpiredOrInvalid', 'This password reset link is invalid or has expired.')} +

+ + {t('auth.backToLogin', 'Back to login')} + +
+
+
+ ) + } + + return ( +
+
+
+
+ +
+ +
+
+ {status === 'success' ? ( +
+
+ + + +
+

+ {t('resetPassword.success', 'Password changed!')} +

+

+ {t('resetPassword.redirectingToLogin', 'Redirecting to login...')} +

+
+
+ ) : ( + <> +

+ {t('resetPassword.title', 'Set new password')} +

+

+ {t('resetPassword.enterNewPassword', 'Enter your new password below.')} +

+ +
+
+ + setPassword(e.target.value)} + placeholder="••••••••" + className="input" + autoComplete="new-password" + disabled={status === 'loading'} + /> +
+ +
+ + setConfirmPassword(e.target.value)} + placeholder="••••••••" + className="input" + autoComplete="new-password" + disabled={status === 'loading'} + /> +
+ + {error && ( +
+ {error} +
+ )} + + +
+ +
+ + {t('auth.backToLogin', 'Back to login')} + +
+ + )} +
+
+
+ ) +} From ea65b4d73ce47f7eb800b1e2e123a5f10b206c40 Mon Sep 17 00:00:00 2001 From: Egor Date: Sun, 25 Jan 2026 10:44:00 +0300 Subject: [PATCH 25/26] Update App.tsx --- src/App.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/App.tsx b/src/App.tsx index 4d87f50..e02e997 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -13,6 +13,7 @@ import TelegramCallback from './pages/TelegramCallback' import TelegramRedirect from './pages/TelegramRedirect' import DeepLinkRedirect from './pages/DeepLinkRedirect' import VerifyEmail from './pages/VerifyEmail' +import ResetPassword from './pages/ResetPassword' // User pages - lazy load const Dashboard = lazy(() => import('./pages/Dashboard')) @@ -118,6 +119,7 @@ function App() { } /> } /> } /> + } /> {/* Protected routes */} Date: Sun, 25 Jan 2026 10:44:24 +0300 Subject: [PATCH 26/26] Add files via upload --- src/locales/ru.json | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/locales/ru.json b/src/locales/ru.json index 8a3ecd2..fc2dd4b 100644 --- a/src/locales/ru.json +++ b/src/locales/ru.json @@ -85,6 +85,10 @@ "invalidEmail": "Введите корректный email адрес", "emailAlreadyRegistered": "Этот email уже зарегистрирован", "invalidCredentials": "Неверный email или пароль", + "forgotPassword": "Забыли пароль?", + "forgotPasswordHint": "Введите email и мы отправим инструкции для сброса пароля.", + "passwordResetSent": "Если аккаунт с таким email существует, мы отправили инструкции по сбросу пароля.", + "sendResetLink": "Отправить ссылку", "tooManyAttempts": "Слишком много попыток. Попробуйте позже", "verificationEmailNotice": "После регистрации на вашу почту будет отправлено письмо для подтверждения", "checkEmail": "Проверьте почту", @@ -103,6 +107,15 @@ "failed": "Ошибка подтверждения", "goToLogin": "Перейти к входу" }, + "resetPassword": { + "title": "Новый пароль", + "enterNewPassword": "Введите новый пароль ниже.", + "success": "Пароль изменён!", + "redirectingToLogin": "Переходим на страницу входа...", + "setPassword": "Установить пароль", + "invalidToken": "Недействительная ссылка", + "tokenExpiredOrInvalid": "Ссылка для сброса пароля недействительна или истекла." + }, "dashboard": { "title": "Личный кабинет", "welcome": "Добро пожаловать, {{name}}!",