mirror of
https://github.com/chillpadclub/bedolaga-cabinet.git
synced 2026-07-29 18:13:47 +00:00
fix: resolve telegram auth token expiration and clean up codebase
- Fix stale initData comparison in clearStaleSessionIfNeeded that destroyed valid refresh tokens on mobile WebView reopens - Restrict X-Telegram-Init-Data header to auth endpoints only - Close Mini App on auth retry to force fresh initData from Telegram - Merge Connection page error/not-configured states for better UX - Remove unnecessary comments across 40+ files (section dividers, restating comments, noise catch block comments) - Configure ESLint allowEmptyCatch to properly handle intentional empty catch blocks (62 warnings resolved)
This commit is contained in:
@@ -147,8 +147,6 @@ export const adminApi = {
|
||||
},
|
||||
};
|
||||
|
||||
// ============ Dashboard Stats Types ============
|
||||
|
||||
export interface NodeStatus {
|
||||
uuid: string;
|
||||
name: string;
|
||||
@@ -239,8 +237,6 @@ export interface DashboardStats {
|
||||
tariff_stats?: TariffStats;
|
||||
}
|
||||
|
||||
// ============ Extended Stats Types ============
|
||||
|
||||
export interface TopReferrerItem {
|
||||
user_id: number;
|
||||
telegram_id: number | null;
|
||||
@@ -318,8 +314,6 @@ export interface SystemInfo {
|
||||
subscriptions_active: number;
|
||||
}
|
||||
|
||||
// ============ Dashboard Stats API ============
|
||||
|
||||
export const statsApi = {
|
||||
// Get system info
|
||||
getSystemInfo: async (): Promise<SystemInfo> => {
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import { apiClient } from './client';
|
||||
|
||||
// ============ Types ============
|
||||
|
||||
// Status & Connection
|
||||
export interface ConnectionStatus {
|
||||
status: string;
|
||||
@@ -224,8 +222,6 @@ export interface SyncResponse {
|
||||
data?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
// ============ API ============
|
||||
|
||||
export const adminRemnawaveApi = {
|
||||
// Status & Connection
|
||||
getStatus: async (): Promise<RemnaWaveStatusResponse> => {
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import apiClient from './client';
|
||||
|
||||
// ============ Types ============
|
||||
|
||||
export interface ReleaseItem {
|
||||
tag_name: string;
|
||||
name: string;
|
||||
@@ -22,8 +20,6 @@ export interface ReleasesResponse {
|
||||
cabinet: ProjectReleasesInfo;
|
||||
}
|
||||
|
||||
// ============ API ============
|
||||
|
||||
export const adminUpdatesApi = {
|
||||
getReleases: async (): Promise<ReleasesResponse> => {
|
||||
const response = await apiClient.get('/cabinet/admin/updates/releases');
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import apiClient from './client';
|
||||
|
||||
// ============ Types ============
|
||||
|
||||
export interface TrafficPurchaseInfo {
|
||||
id: number;
|
||||
traffic_gb: number;
|
||||
@@ -381,8 +379,6 @@ export interface AdminUserGiftsResponse {
|
||||
received_total: number;
|
||||
}
|
||||
|
||||
// ============ API ============
|
||||
|
||||
export const adminUsersApi = {
|
||||
// List users
|
||||
getUsers: async (
|
||||
|
||||
@@ -13,7 +13,6 @@ import type {
|
||||
} from '../types';
|
||||
|
||||
export const authApi = {
|
||||
// Telegram WebApp authentication
|
||||
loginTelegram: async (
|
||||
initData: string,
|
||||
campaignSlug?: string | null,
|
||||
@@ -27,7 +26,6 @@ export const authApi = {
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Telegram Login Widget authentication
|
||||
loginTelegramWidget: async (
|
||||
data: {
|
||||
id: number;
|
||||
@@ -49,7 +47,6 @@ export const authApi = {
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Telegram OIDC authentication (popup flow with id_token)
|
||||
loginTelegramOIDC: async (
|
||||
idToken: string,
|
||||
campaignSlug?: string | null,
|
||||
@@ -63,7 +60,6 @@ export const authApi = {
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Email login
|
||||
loginEmail: async (
|
||||
email: string,
|
||||
password: string,
|
||||
@@ -79,7 +75,6 @@ export const authApi = {
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Register email (link to existing Telegram account)
|
||||
registerEmail: async (
|
||||
email: string,
|
||||
password: string,
|
||||
@@ -91,8 +86,6 @@ export const authApi = {
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Register standalone email account (no Telegram required)
|
||||
// Returns message - user must verify email before login
|
||||
registerEmailStandalone: async (data: {
|
||||
email: string;
|
||||
password: string;
|
||||
@@ -107,7 +100,6 @@ export const authApi = {
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Verify email and get auth tokens
|
||||
verifyEmail: async (token: string, campaignSlug?: string | null): Promise<AuthResponse> => {
|
||||
const response = await apiClient.post<AuthResponse>('/cabinet/auth/email/verify', {
|
||||
token,
|
||||
@@ -116,13 +108,11 @@ export const authApi = {
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Resend verification email
|
||||
resendVerification: async (): Promise<{ message: string }> => {
|
||||
const response = await apiClient.post('/cabinet/auth/email/resend');
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Refresh token
|
||||
refreshToken: async (refreshToken: string): Promise<TokenResponse> => {
|
||||
const response = await apiClient.post<TokenResponse>('/cabinet/auth/refresh', {
|
||||
refresh_token: refreshToken,
|
||||
@@ -130,20 +120,17 @@ export const authApi = {
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Logout
|
||||
logout: async (refreshToken: string): Promise<void> => {
|
||||
await apiClient.post('/cabinet/auth/logout', {
|
||||
refresh_token: refreshToken,
|
||||
});
|
||||
},
|
||||
|
||||
// Forgot password
|
||||
forgotPassword: async (email: string): Promise<{ message: string }> => {
|
||||
const response = await apiClient.post('/cabinet/auth/password/forgot', { email });
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Reset password
|
||||
resetPassword: async (token: string, password: string): Promise<{ message: string }> => {
|
||||
const response = await apiClient.post('/cabinet/auth/password/reset', {
|
||||
token,
|
||||
@@ -152,13 +139,11 @@ export const authApi = {
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Get current user
|
||||
getMe: async (): Promise<User> => {
|
||||
const response = await apiClient.get<User>('/cabinet/auth/me');
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Request email change - sends verification code to new email
|
||||
requestEmailChange: async (
|
||||
newEmail: string,
|
||||
): Promise<{ message: string; new_email: string; expires_in_minutes: number }> => {
|
||||
@@ -168,7 +153,6 @@ export const authApi = {
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Verify email change with code
|
||||
verifyEmailChange: async (code: string): Promise<{ message: string; email: string }> => {
|
||||
const response = await apiClient.post('/cabinet/auth/email/change/verify', {
|
||||
code,
|
||||
@@ -176,7 +160,6 @@ export const authApi = {
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// OAuth: get enabled providers
|
||||
getOAuthProviders: async (): Promise<{ providers: OAuthProvider[] }> => {
|
||||
const response = await apiClient.get<{ providers: OAuthProvider[] }>(
|
||||
'/cabinet/auth/oauth/providers',
|
||||
@@ -184,7 +167,6 @@ export const authApi = {
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// OAuth: get authorization URL
|
||||
getOAuthAuthorizeUrl: async (
|
||||
provider: string,
|
||||
): Promise<{ authorize_url: string; state: string }> => {
|
||||
@@ -194,7 +176,6 @@ export const authApi = {
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// OAuth: callback (exchange code for tokens)
|
||||
oauthCallback: async (
|
||||
provider: string,
|
||||
code: string,
|
||||
@@ -216,7 +197,6 @@ export const authApi = {
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Account linking
|
||||
getLinkedProviders: async (): Promise<LinkedProvidersResponse> => {
|
||||
const response = await apiClient.get<LinkedProvidersResponse>(
|
||||
'/cabinet/auth/account/linked-providers',
|
||||
@@ -248,7 +228,6 @@ export const authApi = {
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Link Telegram account (Mini App initData, OIDC id_token, or Login Widget data)
|
||||
linkTelegram: async (
|
||||
data:
|
||||
| { init_data: string }
|
||||
@@ -270,8 +249,6 @@ export const authApi = {
|
||||
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,
|
||||
@@ -295,13 +272,11 @@ export const authApi = {
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Auto-login from guest purchase success page
|
||||
autoLogin: async (token: string): Promise<AuthResponse> => {
|
||||
const response = await apiClient.post<AuthResponse>('/cabinet/auth/login/auto', { token });
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Account merge (no JWT required)
|
||||
getMergePreview: async (mergeToken: string): Promise<MergePreviewResponse> => {
|
||||
const response = await apiClient.get<MergePreviewResponse>(
|
||||
`/cabinet/auth/merge/${encodeURIComponent(mergeToken)}`,
|
||||
|
||||
@@ -88,9 +88,7 @@ export const getCachedBranding = (): BrandingInfo | null => {
|
||||
export const setCachedBranding = (branding: BrandingInfo) => {
|
||||
try {
|
||||
sessionStorage.setItem(BRANDING_CACHE_KEY, JSON.stringify(branding));
|
||||
} catch {
|
||||
// sessionStorage not available
|
||||
}
|
||||
} catch {}
|
||||
};
|
||||
|
||||
// Preload logo image as blob to hide backend URL
|
||||
|
||||
@@ -11,10 +11,8 @@ import { API } from '../config/constants';
|
||||
|
||||
const API_BASE_URL = import.meta.env.VITE_API_URL || '/api';
|
||||
|
||||
// Настраиваем endpoint для refresh
|
||||
tokenRefreshManager.setRefreshEndpoint(`${API_BASE_URL}/cabinet/auth/refresh`);
|
||||
|
||||
// CSRF token management
|
||||
const CSRF_COOKIE_NAME = 'csrf_token';
|
||||
const CSRF_HEADER_NAME = 'X-CSRF-Token';
|
||||
|
||||
@@ -34,7 +32,6 @@ function ensureCsrfToken(): string {
|
||||
let token = getCsrfToken();
|
||||
if (!token) {
|
||||
token = generateCsrfToken();
|
||||
// Set cookie with SameSite=Strict for CSRF protection
|
||||
document.cookie = `${CSRF_COOKIE_NAME}=${token}; path=/; SameSite=Strict; Secure`;
|
||||
}
|
||||
return token;
|
||||
@@ -49,9 +46,7 @@ const getTelegramInitData = (): string | null => {
|
||||
tokenStorage.setTelegramInitData(raw);
|
||||
return raw;
|
||||
}
|
||||
} catch {
|
||||
// Not in Telegram or SDK not initialized
|
||||
}
|
||||
} catch {}
|
||||
|
||||
return tokenStorage.getTelegramInitData();
|
||||
};
|
||||
@@ -64,7 +59,6 @@ export const apiClient = axios.create({
|
||||
},
|
||||
});
|
||||
|
||||
// Auth endpoints that don't need Bearer token or token refresh
|
||||
const AUTH_ENDPOINTS = [
|
||||
'/cabinet/auth/telegram',
|
||||
'/cabinet/auth/telegram/widget',
|
||||
@@ -85,15 +79,11 @@ function isAuthEndpoint(url: string | undefined): boolean {
|
||||
return AUTH_ENDPOINTS.some((endpoint) => url.startsWith(endpoint));
|
||||
}
|
||||
|
||||
// Request interceptor - add auth token with expiration check
|
||||
apiClient.interceptors.request.use(async (config: InternalAxiosRequestConfig) => {
|
||||
// Skip token refresh and Bearer header for auth endpoints
|
||||
// These endpoints authenticate via init_data/credentials, not Bearer tokens
|
||||
if (!isAuthEndpoint(config.url)) {
|
||||
let token = tokenStorage.getAccessToken();
|
||||
|
||||
if (token && isTokenExpired(token)) {
|
||||
// Access token expired — try refresh
|
||||
const newToken = await tokenRefreshManager.refreshAccessToken();
|
||||
if (newToken) {
|
||||
token = newToken;
|
||||
@@ -103,7 +93,6 @@ apiClient.interceptors.request.use(async (config: InternalAxiosRequestConfig) =>
|
||||
return config;
|
||||
}
|
||||
} else if (!token && tokenStorage.getRefreshToken()) {
|
||||
// No access token (e.g. tab reopen) but refresh token exists — restore session
|
||||
const newToken = await tokenRefreshManager.refreshAccessToken();
|
||||
if (newToken) {
|
||||
token = newToken;
|
||||
@@ -115,12 +104,16 @@ apiClient.interceptors.request.use(async (config: InternalAxiosRequestConfig) =>
|
||||
}
|
||||
}
|
||||
|
||||
const telegramInitData = getTelegramInitData();
|
||||
if (telegramInitData && config.headers) {
|
||||
config.headers['X-Telegram-Init-Data'] = telegramInitData;
|
||||
const isTelegramAuthEndpoint =
|
||||
config.url?.startsWith('/cabinet/auth/telegram') ||
|
||||
config.url?.startsWith('/cabinet/auth/account/link/telegram');
|
||||
if (isTelegramAuthEndpoint) {
|
||||
const telegramInitData = getTelegramInitData();
|
||||
if (telegramInitData && config.headers) {
|
||||
config.headers['X-Telegram-Init-Data'] = telegramInitData;
|
||||
}
|
||||
}
|
||||
|
||||
// Add CSRF token for state-changing methods
|
||||
const method = config.method?.toUpperCase();
|
||||
if (method && ['POST', 'PUT', 'DELETE', 'PATCH'].includes(method) && config.headers) {
|
||||
config.headers[CSRF_HEADER_NAME] = ensureCsrfToken();
|
||||
@@ -129,7 +122,6 @@ apiClient.interceptors.request.use(async (config: InternalAxiosRequestConfig) =>
|
||||
return config;
|
||||
});
|
||||
|
||||
// Custom error types for special handling
|
||||
export interface MaintenanceError {
|
||||
code: 'maintenance';
|
||||
message: string;
|
||||
@@ -180,13 +172,11 @@ export function isBlacklistedError(
|
||||
return err.response?.status === 403 && err.response?.data?.detail?.code === 'blacklisted';
|
||||
}
|
||||
|
||||
// Response interceptor - handle 401, 503 (maintenance), 403 (channel subscription)
|
||||
apiClient.interceptors.response.use(
|
||||
(response) => response,
|
||||
async (error: AxiosError) => {
|
||||
const originalRequest = error.config as InternalAxiosRequestConfig & { _retry?: boolean };
|
||||
|
||||
// Handle maintenance mode (503)
|
||||
if (isMaintenanceError(error)) {
|
||||
const detail = (error.response?.data as { detail: MaintenanceError }).detail;
|
||||
useBlockingStore.getState().setMaintenance({
|
||||
@@ -196,7 +186,6 @@ apiClient.interceptors.response.use(
|
||||
return Promise.reject(error);
|
||||
}
|
||||
|
||||
// Handle channel subscription required (403)
|
||||
if (isChannelSubscriptionError(error)) {
|
||||
const detail = (error.response?.data as { detail: ChannelSubscriptionError }).detail;
|
||||
useBlockingStore.getState().setChannelSubscription({
|
||||
@@ -207,7 +196,6 @@ apiClient.interceptors.response.use(
|
||||
return Promise.reject(error);
|
||||
}
|
||||
|
||||
// Handle blacklisted user (403)
|
||||
if (isBlacklistedError(error)) {
|
||||
const detail = (error.response?.data as { detail: BlacklistedError }).detail;
|
||||
useBlockingStore.getState().setBlacklisted({
|
||||
@@ -216,14 +204,10 @@ apiClient.interceptors.response.use(
|
||||
return Promise.reject(error);
|
||||
}
|
||||
|
||||
// Если получили 401 и ещё не пробовали refresh (на случай если проверка exp не сработала)
|
||||
if (error.response?.status === 401 && !originalRequest._retry) {
|
||||
// Не обрабатываем 401 для авторизационных endpoints - пусть ошибка дойдет до компонента
|
||||
const requestUrl = originalRequest.url || '';
|
||||
const isLoginEndpoint = isAuthEndpoint(requestUrl);
|
||||
|
||||
if (isLoginEndpoint) {
|
||||
// Пробрасываем ошибку в компонент для показа сообщения пользователю
|
||||
if (isAuthEndpoint(requestUrl)) {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
|
||||
@@ -236,7 +220,6 @@ apiClient.interceptors.response.use(
|
||||
}
|
||||
return apiClient(originalRequest);
|
||||
} else {
|
||||
// Refresh не удался
|
||||
tokenStorage.clearTokens();
|
||||
safeRedirectToLogin();
|
||||
}
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
import apiClient from './client';
|
||||
import type { AnimationConfig } from '@/components/ui/backgrounds/types';
|
||||
|
||||
// ============================================================
|
||||
// Public types
|
||||
// ============================================================
|
||||
|
||||
export interface LandingFeature {
|
||||
icon: string;
|
||||
title: string;
|
||||
@@ -130,10 +126,6 @@ export interface PurchaseStatus {
|
||||
bot_link: string | null;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Locale helpers
|
||||
// ============================================================
|
||||
|
||||
/** Locale dict for multi-language text fields (admin API) */
|
||||
export type LocaleDict = Record<string, string>;
|
||||
|
||||
@@ -148,10 +140,6 @@ export const LOCALE_META: Record<SupportedLocale, { flag: string; name: string;
|
||||
fa: { flag: '\u{1F1EE}\u{1F1F7}', name: 'FA', rtl: true },
|
||||
};
|
||||
|
||||
// ============================================================
|
||||
// Admin types
|
||||
// ============================================================
|
||||
|
||||
/** Admin feature type with localized title/description */
|
||||
export interface AdminLandingFeature {
|
||||
icon: string;
|
||||
@@ -232,12 +220,7 @@ export interface LandingCreateRequest {
|
||||
|
||||
export type LandingUpdateRequest = Partial<LandingCreateRequest>;
|
||||
|
||||
/**
|
||||
* Normalize a value that might be a plain string (old API) or a LocaleDict.
|
||||
* If it's a string, wraps it as `{ ru: value }`.
|
||||
* If null/undefined, returns the fallback.
|
||||
*/
|
||||
/** Extract best display string from a LocaleDict: ru → en → first available → '' */
|
||||
/** Extract best display string from a LocaleDict: ru -> en -> first available -> '' */
|
||||
export function resolveLocaleDisplay(dict: LocaleDict | string | null | undefined): string {
|
||||
if (!dict) return '';
|
||||
if (typeof dict === 'string') return dict;
|
||||
@@ -253,10 +236,6 @@ export function toLocaleDict(
|
||||
return value;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Public API
|
||||
// ============================================================
|
||||
|
||||
export const landingApi = {
|
||||
getConfig: async (slug: string, lang?: string): Promise<LandingConfig> => {
|
||||
const params = lang ? `?lang=${lang}` : '';
|
||||
@@ -280,10 +259,6 @@ export const landingApi = {
|
||||
},
|
||||
};
|
||||
|
||||
// ============================================================
|
||||
// Admin stats types
|
||||
// ============================================================
|
||||
|
||||
export interface LandingDailyStat {
|
||||
date: string;
|
||||
purchases: number;
|
||||
@@ -311,10 +286,6 @@ export interface LandingStatsResponse {
|
||||
tariff_stats: LandingTariffStat[];
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Admin purchase list types
|
||||
// ============================================================
|
||||
|
||||
export type PurchaseItemStatus =
|
||||
| 'pending'
|
||||
| 'paid'
|
||||
@@ -346,10 +317,6 @@ export interface LandingPurchaseListResponse {
|
||||
total: number;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Admin API
|
||||
// ============================================================
|
||||
|
||||
export const adminLandingsApi = {
|
||||
list: async (): Promise<LandingListItem[]> => {
|
||||
const response = await apiClient.get('/cabinet/admin/landings');
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import apiClient from './client';
|
||||
|
||||
// === Types ===
|
||||
|
||||
export interface AdminRole {
|
||||
id: number;
|
||||
name: string;
|
||||
@@ -144,8 +142,6 @@ export interface PermissionSection {
|
||||
actions: string[];
|
||||
}
|
||||
|
||||
// === API ===
|
||||
|
||||
const BASE = '/cabinet/admin/rbac';
|
||||
|
||||
export const rbacApi = {
|
||||
|
||||
@@ -12,19 +12,16 @@ import type {
|
||||
} from '../types';
|
||||
|
||||
export const subscriptionApi = {
|
||||
// Get current subscription status
|
||||
getSubscription: async (): Promise<SubscriptionStatusResponse> => {
|
||||
const response = await apiClient.get<SubscriptionStatusResponse>('/cabinet/subscription');
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Get renewal options
|
||||
getRenewalOptions: async (): Promise<RenewalOption[]> => {
|
||||
const response = await apiClient.get<RenewalOption[]>('/cabinet/subscription/renewal-options');
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Renew subscription
|
||||
renewSubscription: async (
|
||||
periodDays: number,
|
||||
): Promise<{
|
||||
@@ -38,7 +35,6 @@ export const subscriptionApi = {
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Get traffic packages
|
||||
getTrafficPackages: async (): Promise<TrafficPackage[]> => {
|
||||
const response = await apiClient.get<TrafficPackage[]>(
|
||||
'/cabinet/subscription/traffic-packages',
|
||||
@@ -46,7 +42,6 @@ export const subscriptionApi = {
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Purchase traffic
|
||||
purchaseTraffic: async (
|
||||
gb: number,
|
||||
): Promise<{
|
||||
@@ -58,7 +53,6 @@ export const subscriptionApi = {
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Purchase devices
|
||||
purchaseDevices: async (
|
||||
devices: number,
|
||||
): Promise<{
|
||||
@@ -75,7 +69,6 @@ export const subscriptionApi = {
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Get device purchase price
|
||||
getDevicePrice: async (
|
||||
devices: number = 1,
|
||||
): Promise<{
|
||||
@@ -103,12 +96,10 @@ export const subscriptionApi = {
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Save devices cart for later purchase after top-up
|
||||
saveDevicesCart: async (devices: number): Promise<void> => {
|
||||
await apiClient.post('/cabinet/subscription/devices/save-cart', { devices });
|
||||
},
|
||||
|
||||
// Get device reduction info (for reducing device limit)
|
||||
getDeviceReductionInfo: async (): Promise<{
|
||||
available: boolean;
|
||||
reason?: string;
|
||||
@@ -121,7 +112,6 @@ export const subscriptionApi = {
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Reduce device limit
|
||||
reduceDevices: async (
|
||||
newDeviceLimit: number,
|
||||
): Promise<{
|
||||
@@ -136,12 +126,10 @@ export const subscriptionApi = {
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Save traffic cart for later purchase after top-up
|
||||
saveTrafficCart: async (trafficGb: number): Promise<void> => {
|
||||
await apiClient.post('/cabinet/subscription/traffic/save-cart', { gb: trafficGb });
|
||||
},
|
||||
|
||||
// Update autopay settings
|
||||
updateAutopay: async (
|
||||
enabled: boolean,
|
||||
daysBefore?: number,
|
||||
@@ -157,25 +145,21 @@ export const subscriptionApi = {
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Get trial info
|
||||
getTrialInfo: async (): Promise<TrialInfo> => {
|
||||
const response = await apiClient.get<TrialInfo>('/cabinet/subscription/trial');
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Activate trial
|
||||
activateTrial: async (): Promise<Subscription> => {
|
||||
const response = await apiClient.post<Subscription>('/cabinet/subscription/trial');
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Get purchase options (periods, servers, traffic, devices)
|
||||
getPurchaseOptions: async (): Promise<PurchaseOptions> => {
|
||||
const response = await apiClient.get<PurchaseOptions>('/cabinet/subscription/purchase-options');
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Preview purchase price
|
||||
previewPurchase: async (selection: PurchaseSelection): Promise<PurchasePreview> => {
|
||||
const response = await apiClient.post<PurchasePreview>(
|
||||
'/cabinet/subscription/purchase-preview',
|
||||
@@ -186,7 +170,6 @@ export const subscriptionApi = {
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Submit purchase
|
||||
submitPurchase: async (
|
||||
selection: PurchaseSelection,
|
||||
): Promise<{
|
||||
@@ -201,7 +184,6 @@ export const subscriptionApi = {
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Purchase tariff (for tariffs mode)
|
||||
purchaseTariff: async (
|
||||
tariffId: number,
|
||||
periodDays: number,
|
||||
@@ -223,13 +205,11 @@ export const subscriptionApi = {
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Get app config for connection
|
||||
getAppConfig: async (): Promise<AppConfig> => {
|
||||
const response = await apiClient.get<AppConfig>('/cabinet/subscription/app-config');
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Get available countries/servers
|
||||
getCountries: async (): Promise<{
|
||||
countries: Array<{
|
||||
uuid: string;
|
||||
@@ -253,7 +233,6 @@ export const subscriptionApi = {
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Update countries/servers
|
||||
updateCountries: async (
|
||||
countries: string[],
|
||||
): Promise<{
|
||||
@@ -267,7 +246,6 @@ export const subscriptionApi = {
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Get connection link and instructions
|
||||
getConnectionLink: async (): Promise<{
|
||||
subscription_url: string | null;
|
||||
display_link: string | null;
|
||||
@@ -283,7 +261,6 @@ export const subscriptionApi = {
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Get hApp download links
|
||||
getHappDownloads: async (): Promise<{
|
||||
platforms: Record<
|
||||
string,
|
||||
@@ -299,9 +276,6 @@ export const subscriptionApi = {
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// ============ Device Management ============
|
||||
|
||||
// Get connected devices
|
||||
getDevices: async (): Promise<{
|
||||
devices: Array<{
|
||||
hwid: string;
|
||||
@@ -316,7 +290,6 @@ export const subscriptionApi = {
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Delete a specific device
|
||||
deleteDevice: async (
|
||||
hwid: string,
|
||||
): Promise<{
|
||||
@@ -330,7 +303,6 @@ export const subscriptionApi = {
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Delete all devices
|
||||
deleteAllDevices: async (): Promise<{
|
||||
success: boolean;
|
||||
message: string;
|
||||
@@ -340,9 +312,6 @@ export const subscriptionApi = {
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// ============ Tariff Switch ============
|
||||
|
||||
// Preview tariff switch cost
|
||||
previewTariffSwitch: async (
|
||||
tariffId: number,
|
||||
): Promise<{
|
||||
@@ -372,7 +341,6 @@ export const subscriptionApi = {
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Switch to a different tariff
|
||||
switchTariff: async (
|
||||
tariffId: number,
|
||||
): Promise<{
|
||||
@@ -393,9 +361,6 @@ export const subscriptionApi = {
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// ============ Subscription Pause (Daily Tariffs) ============
|
||||
|
||||
// Toggle pause/resume for daily subscription
|
||||
togglePause: async (): Promise<{
|
||||
success: boolean;
|
||||
message: string;
|
||||
@@ -407,9 +372,6 @@ export const subscriptionApi = {
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// ============ Traffic Switch ============
|
||||
|
||||
// Switch to a different traffic package
|
||||
switchTraffic: async (
|
||||
gb: number,
|
||||
): Promise<{
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import apiClient from './client';
|
||||
|
||||
// ==================== TYPES ====================
|
||||
|
||||
export interface WheelPrize {
|
||||
id: number;
|
||||
display_name: string;
|
||||
@@ -79,7 +77,6 @@ export interface StarsInvoiceResponse {
|
||||
stars_amount: number;
|
||||
}
|
||||
|
||||
// Admin types
|
||||
export interface WheelPrizeAdmin {
|
||||
id: number;
|
||||
config_id: number;
|
||||
@@ -99,7 +96,6 @@ export interface WheelPrizeAdmin {
|
||||
updated_at: string | null;
|
||||
}
|
||||
|
||||
// Type for creating a new prize (excludes id, config_id which are auto-generated)
|
||||
export interface CreateWheelPrizeData {
|
||||
prize_type: string;
|
||||
prize_value: number;
|
||||
@@ -180,22 +176,17 @@ export interface AdminSpinsResponse {
|
||||
pages: number;
|
||||
}
|
||||
|
||||
// ==================== USER API ====================
|
||||
|
||||
export const wheelApi = {
|
||||
// Get wheel config
|
||||
getConfig: async (): Promise<WheelConfig> => {
|
||||
const response = await apiClient.get<WheelConfig>('/cabinet/wheel/config');
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Check spin availability
|
||||
checkAvailability: async (): Promise<SpinAvailability> => {
|
||||
const response = await apiClient.get<SpinAvailability>('/cabinet/wheel/availability');
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Spin the wheel
|
||||
spin: async (paymentType: 'telegram_stars' | 'subscription_days'): Promise<SpinResult> => {
|
||||
const response = await apiClient.post<SpinResult>('/cabinet/wheel/spin', {
|
||||
payment_type: paymentType,
|
||||
@@ -203,7 +194,6 @@ export const wheelApi = {
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Get spin history
|
||||
getHistory: async (page = 1, perPage = 20): Promise<SpinHistoryResponse> => {
|
||||
const response = await apiClient.get<SpinHistoryResponse>('/cabinet/wheel/history', {
|
||||
params: { page, per_page: perPage },
|
||||
@@ -211,41 +201,33 @@ export const wheelApi = {
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Create Stars invoice for Mini App payment
|
||||
createStarsInvoice: async (): Promise<StarsInvoiceResponse> => {
|
||||
const response = await apiClient.post<StarsInvoiceResponse>('/cabinet/wheel/stars-invoice');
|
||||
return response.data;
|
||||
},
|
||||
};
|
||||
|
||||
// ==================== ADMIN API ====================
|
||||
|
||||
export const adminWheelApi = {
|
||||
// Get full config
|
||||
getConfig: async (): Promise<AdminWheelConfig> => {
|
||||
const response = await apiClient.get<AdminWheelConfig>('/cabinet/admin/wheel/config');
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Update config
|
||||
updateConfig: async (data: Partial<AdminWheelConfig>): Promise<AdminWheelConfig> => {
|
||||
const response = await apiClient.put<AdminWheelConfig>('/cabinet/admin/wheel/config', data);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Get prizes
|
||||
getPrizes: async (): Promise<WheelPrizeAdmin[]> => {
|
||||
const response = await apiClient.get<WheelPrizeAdmin[]>('/cabinet/admin/wheel/prizes');
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Create prize
|
||||
createPrize: async (data: CreateWheelPrizeData): Promise<WheelPrizeAdmin> => {
|
||||
const response = await apiClient.post<WheelPrizeAdmin>('/cabinet/admin/wheel/prizes', data);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Update prize
|
||||
updatePrize: async (
|
||||
prizeId: number,
|
||||
data: Partial<WheelPrizeAdmin>,
|
||||
@@ -257,17 +239,14 @@ export const adminWheelApi = {
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Delete prize
|
||||
deletePrize: async (prizeId: number): Promise<void> => {
|
||||
await apiClient.delete(`/cabinet/admin/wheel/prizes/${prizeId}`);
|
||||
},
|
||||
|
||||
// Reorder prizes
|
||||
reorderPrizes: async (prizeIds: number[]): Promise<void> => {
|
||||
await apiClient.post('/cabinet/admin/wheel/prizes/reorder', { prize_ids: prizeIds });
|
||||
},
|
||||
|
||||
// Get statistics
|
||||
getStatistics: async (dateFrom?: string, dateTo?: string): Promise<WheelStatistics> => {
|
||||
const response = await apiClient.get<WheelStatistics>('/cabinet/admin/wheel/statistics', {
|
||||
params: { date_from: dateFrom, date_to: dateTo },
|
||||
@@ -275,7 +254,6 @@ export const adminWheelApi = {
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Get all spins
|
||||
getSpins: async (params?: {
|
||||
user_id?: number;
|
||||
date_from?: string;
|
||||
|
||||
Reference in New Issue
Block a user