(null);
+
+ const handleOAuthLogin = async (provider: string) => {
+ setError('');
+ setOauthLoading(provider);
+ try {
+ const { authorize_url, state } = await authApi.getOAuthAuthorizeUrl(provider);
+ saveOAuthState(state, provider);
+ window.location.href = authorize_url;
+ } catch {
+ setError(t('auth.oauthError', 'Authorization was denied or failed'));
+ setOauthLoading(null);
+ }
+ };
+
const botUsername = import.meta.env.VITE_TELEGRAM_BOT_USERNAME || '';
// If email auth is disabled but user came with ref param, redirect to bot
@@ -424,6 +449,38 @@ export default function Login() {
)}
+ {/* OAuth providers section */}
+ {oauthProviders.length > 0 && (
+ <>
+
+
+
{t('auth.or', 'or')}
+
+
+
+ {oauthProviders.map((provider) => (
+
+ ))}
+
+ >
+ )}
+
{/* Email auth section - only when enabled */}
{isEmailAuthEnabled && (
<>
diff --git a/src/pages/OAuthCallback.tsx b/src/pages/OAuthCallback.tsx
new file mode 100644
index 0000000..7078fc5
--- /dev/null
+++ b/src/pages/OAuthCallback.tsx
@@ -0,0 +1,119 @@
+import { useEffect, useState } from 'react';
+import { useNavigate, useSearchParams } from 'react-router';
+import { useTranslation } from 'react-i18next';
+import { useAuthStore } from '../store/auth';
+
+// SessionStorage helpers for OAuth state
+const OAUTH_STATE_KEY = 'oauth_state';
+const OAUTH_PROVIDER_KEY = 'oauth_provider';
+
+export function saveOAuthState(state: string, provider: string): void {
+ sessionStorage.setItem(OAUTH_STATE_KEY, state);
+ sessionStorage.setItem(OAUTH_PROVIDER_KEY, provider);
+}
+
+export function getAndClearOAuthState(): { state: string; provider: string } | null {
+ const state = sessionStorage.getItem(OAUTH_STATE_KEY);
+ const provider = sessionStorage.getItem(OAUTH_PROVIDER_KEY);
+ sessionStorage.removeItem(OAUTH_STATE_KEY);
+ sessionStorage.removeItem(OAUTH_PROVIDER_KEY);
+ if (!state || !provider) return null;
+ return { state, provider };
+}
+
+export default function OAuthCallback() {
+ const { t } = useTranslation();
+ const navigate = useNavigate();
+ const [searchParams] = useSearchParams();
+ const [error, setError] = useState('');
+ const { loginWithOAuth, isAuthenticated } = useAuthStore();
+
+ useEffect(() => {
+ if (isAuthenticated) {
+ navigate('/', { replace: true });
+ return;
+ }
+
+ const authenticate = async () => {
+ const code = searchParams.get('code');
+ const urlState = searchParams.get('state');
+
+ if (!code || !urlState) {
+ setError(t('auth.oauthError', 'Authorization was denied or failed'));
+ return;
+ }
+
+ // Get saved state from sessionStorage
+ const saved = getAndClearOAuthState();
+ if (!saved) {
+ setError(t('auth.oauthExpired', 'OAuth session expired. Please try again.'));
+ return;
+ }
+
+ // Validate state match
+ if (saved.state !== urlState) {
+ setError(t('auth.oauthError', 'Authorization was denied or failed'));
+ return;
+ }
+
+ try {
+ await loginWithOAuth(saved.provider, code, urlState);
+ navigate('/', { replace: true });
+ } catch (err: unknown) {
+ const error = err as { response?: { data?: { detail?: string } } };
+ setError(
+ error.response?.data?.detail ||
+ t('auth.oauthError', 'Authorization was denied or failed'),
+ );
+ }
+ };
+
+ authenticate();
+ }, [searchParams, loginWithOAuth, navigate, isAuthenticated, t]);
+
+ if (error) {
+ return (
+
+
+
+
+
+
{t('auth.loginFailed')}
+
{error}
+
+
+
+
+ );
+ }
+
+ return (
+
+
+
+
+
{t('auth.authenticating')}
+
{t('common.loading')}
+
+
+ );
+}
diff --git a/src/store/auth.ts b/src/store/auth.ts
index 91b2fd0..56364bb 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;
+ loginWithOAuth: (provider: string, code: string, state: string) => Promise;
registerWithEmail: (
email: string,
password: string,
@@ -267,6 +268,18 @@ export const useAuthStore = create()(
await get().checkAdminStatus();
},
+ loginWithOAuth: async (provider, code, state) => {
+ const response = await authApi.oauthCallback(provider, code, state);
+ tokenStorage.setTokens(response.access_token, response.refresh_token);
+ set({
+ accessToken: response.access_token,
+ refreshToken: response.refresh_token,
+ user: response.user,
+ isAuthenticated: true,
+ });
+ await get().checkAdminStatus();
+ },
+
registerWithEmail: async (email, password, firstName, referralCode) => {
// Registration now returns message, not tokens
// User must verify email before they can login
diff --git a/src/types/index.ts b/src/types/index.ts
index 4d45348..23bc36e 100644
--- a/src/types/index.ts
+++ b/src/types/index.ts
@@ -12,7 +12,13 @@ export interface User {
referral_code: string | null;
language: string;
created_at: string;
- auth_type: 'telegram' | 'email'; // Тип аутентификации
+ auth_type: 'telegram' | 'email' | 'google' | 'yandex' | 'discord' | 'vk'; // Тип аутентификации
+}
+
+// OAuth types
+export interface OAuthProvider {
+ name: string;
+ display_name: string;
}
// Auth types