diff --git a/index.html b/index.html index a79985d..75f488b 100644 --- a/index.html +++ b/index.html @@ -11,6 +11,7 @@ + Loading... diff --git a/src/App.tsx b/src/App.tsx index e03ee71..26e0dad 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -39,6 +39,9 @@ const Connection = lazy(() => import('./pages/Connection')); const ConnectionQR = lazy(() => import('./pages/ConnectionQR')); const TopUpMethodSelect = lazy(() => import('./pages/TopUpMethodSelect')); const TopUpAmount = lazy(() => import('./pages/TopUpAmount')); +const ConnectedAccounts = lazy(() => import('./pages/ConnectedAccounts')); +const LinkTelegramCallback = lazy(() => import('./pages/LinkTelegramCallback')); +const MergeAccounts = lazy(() => import('./pages/MergeAccounts')); // Admin pages - lazy load (only for admins) const AdminPanel = lazy(() => import('./pages/AdminPanel')); @@ -183,6 +186,14 @@ function App() { } /> } /> } /> + + + + } + /> {/* Protected routes */} } /> + + + + + + } + /> + + + + + + } + /> + diff --git a/src/api/auth.ts b/src/api/auth.ts index dd010bf..bd5e1e6 100644 --- a/src/api/auth.ts +++ b/src/api/auth.ts @@ -1,5 +1,16 @@ import apiClient from './client'; -import type { AuthResponse, OAuthProvider, RegisterResponse, TokenResponse, User } from '../types'; +import type { + AuthResponse, + LinkCallbackResponse, + LinkedProvidersResponse, + MergePreviewResponse, + MergeResponse, + OAuthProvider, + RegisterResponse, + ServerCompleteResponse, + TokenResponse, + User, +} from '../types'; export const authApi = { // Telegram WebApp authentication @@ -190,4 +201,103 @@ export const authApi = { ); return response.data; }, + + // Account linking + getLinkedProviders: async (): Promise => { + const response = await apiClient.get( + '/cabinet/auth/account/linked-providers', + ); + return response.data; + }, + + linkProviderInit: async (provider: string): Promise<{ authorize_url: string; state: string }> => { + const response = await apiClient.get<{ authorize_url: string; state: string }>( + `/cabinet/auth/account/link/${encodeURIComponent(provider)}/init`, + ); + return response.data; + }, + + linkProviderCallback: async ( + provider: string, + code: string, + state: string, + deviceId?: string, + ): Promise => { + const response = await apiClient.post( + `/cabinet/auth/account/link/${encodeURIComponent(provider)}/callback`, + { + code, + state, + device_id: deviceId, + }, + ); + return response.data; + }, + + // Link Telegram account (Mini App initData or Login Widget data) + linkTelegram: async ( + data: + | { init_data: string } + | { + id: number; + first_name: string; + last_name?: string; + username?: string; + photo_url?: string; + auth_date: number; + hash: string; + }, + ): Promise => { + const response = await apiClient.post( + '/cabinet/auth/account/link/telegram', + data, + ); + return response.data; + }, + + // Server-side OAuth linking completion (no JWT needed — auth via state token) + // Used when OAuth opens in external browser from Telegram Mini App + linkServerComplete: async ( + code: string, + state: string, + deviceId?: string, + ): Promise => { + const response = await apiClient.post( + '/cabinet/auth/account/link/server-complete', + { + code, + state, + device_id: deviceId, + }, + ); + return response.data; + }, + + unlinkProvider: async (provider: string): Promise<{ success: boolean }> => { + const response = await apiClient.post<{ success: boolean }>( + `/cabinet/auth/account/unlink/${encodeURIComponent(provider)}`, + ); + return response.data; + }, + + // Account merge (no JWT required) + getMergePreview: async (mergeToken: string): Promise => { + const response = await apiClient.get( + `/cabinet/auth/merge/${encodeURIComponent(mergeToken)}`, + ); + return response.data; + }, + + executeMerge: async ( + mergeToken: string, + keepSubscriptionFrom: number, + ): Promise => { + const response = await apiClient.post( + `/cabinet/auth/merge/${encodeURIComponent(mergeToken)}`, + { + keep_subscription_from: keepSubscriptionFrom, + }, + ); + return response.data; + }, }; diff --git a/src/api/client.ts b/src/api/client.ts index 0b344a0..1f27460 100644 --- a/src/api/client.ts +++ b/src/api/client.ts @@ -75,11 +75,13 @@ const AUTH_ENDPOINTS = [ '/cabinet/auth/password/forgot', '/cabinet/auth/password/reset', '/cabinet/auth/oauth/', + '/cabinet/auth/merge/', + '/cabinet/auth/account/link/server-complete', ]; function isAuthEndpoint(url: string | undefined): boolean { if (!url) return false; - return AUTH_ENDPOINTS.some((endpoint) => url.includes(endpoint)); + return AUTH_ENDPOINTS.some((endpoint) => url.startsWith(endpoint)); } // Request interceptor - add auth token with expiration check @@ -212,15 +214,10 @@ apiClient.interceptors.response.use( // Если получили 401 и ещё не пробовали refresh (на случай если проверка exp не сработала) if (error.response?.status === 401 && !originalRequest._retry) { // Не обрабатываем 401 для авторизационных endpoints - пусть ошибка дойдет до компонента - const authEndpoints = [ - '/cabinet/auth/email/login', - '/cabinet/auth/telegram', - '/cabinet/auth/telegram/widget', - ]; const requestUrl = originalRequest.url || ''; - const isAuthEndpoint = authEndpoints.some((endpoint) => requestUrl.includes(endpoint)); + const isLoginEndpoint = isAuthEndpoint(requestUrl); - if (isAuthEndpoint) { + if (isLoginEndpoint) { // Пробрасываем ошибку в компонент для показа сообщения пользователю return Promise.reject(error); } diff --git a/src/components/OAuthProviderIcon.tsx b/src/components/OAuthProviderIcon.tsx index 11eeff8..c702ad9 100644 --- a/src/components/OAuthProviderIcon.tsx +++ b/src/components/OAuthProviderIcon.tsx @@ -10,7 +10,7 @@ export default function OAuthProviderIcon({ switch (provider) { case 'google': return ( - +