Add files via upload

This commit is contained in:
Egor
2026-01-15 19:18:17 +03:00
committed by GitHub
parent b118008cdc
commit 7be6b5c0ae
70 changed files with 21835 additions and 0 deletions

206
src/api/admin.ts Normal file
View File

@@ -0,0 +1,206 @@
import apiClient from './client'
export interface AdminTicketUser {
id: number
telegram_id: number
username: string | null
first_name: string | null
last_name: string | null
}
export interface AdminTicketMessage {
id: number
message_text: string
is_from_admin: boolean
has_media: boolean
media_type: string | null
media_file_id: string | null
media_caption: string | null
created_at: string
}
export interface AdminTicket {
id: number
title: string
status: string
priority: string
created_at: string
updated_at: string
closed_at: string | null
messages_count: number
user: AdminTicketUser | null
last_message: AdminTicketMessage | null
}
export interface AdminTicketDetail {
id: number
title: string
status: string
priority: string
created_at: string
updated_at: string
closed_at: string | null
is_reply_blocked: boolean
user: AdminTicketUser | null
messages: AdminTicketMessage[]
}
export interface AdminTicketStats {
total: number
open: number
pending: number
answered: number
closed: number
}
export interface AdminTicketListResponse {
items: AdminTicket[]
total: number
page: number
per_page: number
pages: number
}
export const adminApi = {
// Check if current user is admin
checkIsAdmin: async (): Promise<{ is_admin: boolean }> => {
const response = await apiClient.get('/cabinet/auth/me/is-admin')
return response.data
},
// Get ticket statistics
getTicketStats: async (): Promise<AdminTicketStats> => {
const response = await apiClient.get('/cabinet/admin/tickets/stats')
return response.data
},
// Get all tickets
getTickets: async (params: {
page?: number
per_page?: number
status?: string
priority?: string
} = {}): Promise<AdminTicketListResponse> => {
const response = await apiClient.get('/cabinet/admin/tickets', { params })
return response.data
},
// Get single ticket with messages
getTicket: async (ticketId: number): Promise<AdminTicketDetail> => {
const response = await apiClient.get(`/cabinet/admin/tickets/${ticketId}`)
return response.data
},
// Reply to ticket
replyToTicket: async (ticketId: number, message: string): Promise<AdminTicketMessage> => {
const response = await apiClient.post(`/cabinet/admin/tickets/${ticketId}/reply`, { message })
return response.data
},
// Update ticket status
updateTicketStatus: async (ticketId: number, status: string): Promise<AdminTicketDetail> => {
const response = await apiClient.post(`/cabinet/admin/tickets/${ticketId}/status`, { status })
return response.data
},
// Update ticket priority
updateTicketPriority: async (ticketId: number, priority: string): Promise<AdminTicketDetail> => {
const response = await apiClient.post(`/cabinet/admin/tickets/${ticketId}/priority`, { priority })
return response.data
},
}
// ============ Dashboard Stats Types ============
export interface NodeStatus {
uuid: string
name: string
address: string
is_connected: boolean
is_disabled: boolean
users_online: number
traffic_used_bytes?: number
uptime?: string
}
export interface NodesOverview {
total: number
online: number
offline: number
disabled: number
total_users_online: number
nodes: NodeStatus[]
}
export interface RevenueData {
date: string
amount_kopeks: number
amount_rubles: number
}
export interface SubscriptionStats {
total: number
active: number
trial: number
paid: number
expired: number
purchased_today: number
purchased_week: number
purchased_month: number
trial_to_paid_conversion: number
}
export interface FinancialStats {
income_today_kopeks: number
income_today_rubles: number
income_month_kopeks: number
income_month_rubles: number
income_total_kopeks: number
income_total_rubles: number
subscription_income_kopeks: number
subscription_income_rubles: number
}
export interface ServerStats {
total_servers: number
available_servers: number
servers_with_connections: number
total_revenue_kopeks: number
total_revenue_rubles: number
}
export interface DashboardStats {
nodes: NodesOverview
subscriptions: SubscriptionStats
financial: FinancialStats
servers: ServerStats
revenue_chart: RevenueData[]
}
// ============ Dashboard Stats API ============
export const statsApi = {
// Get complete dashboard stats
getDashboardStats: async (): Promise<DashboardStats> => {
const response = await apiClient.get('/cabinet/admin/stats/dashboard')
return response.data
},
// Get nodes status
getNodesStatus: async (): Promise<NodesOverview> => {
const response = await apiClient.get('/cabinet/admin/stats/nodes')
return response.data
},
// Restart a node
restartNode: async (nodeUuid: string): Promise<{ success: boolean; message: string }> => {
const response = await apiClient.post(`/cabinet/admin/stats/nodes/${nodeUuid}/restart`)
return response.data
},
// Toggle node (enable/disable)
toggleNode: async (nodeUuid: string): Promise<{ success: boolean; message: string; is_disabled: boolean }> => {
const response = await apiClient.post(`/cabinet/admin/stats/nodes/${nodeUuid}/toggle`)
return response.data
},
}

119
src/api/adminApps.ts Normal file
View File

@@ -0,0 +1,119 @@
import apiClient from './client'
export interface LocalizedText {
en: string
ru: string
zh?: string
fa?: string
}
export interface AppButton {
buttonLink: string
buttonText: LocalizedText
}
export interface AppStep {
description: LocalizedText
buttons?: AppButton[]
title?: LocalizedText
}
export interface AppDefinition {
id: string
name: string
isFeatured: boolean
urlScheme: string
isNeedBase64Encoding?: boolean
installationStep: AppStep
addSubscriptionStep: AppStep
connectAndUseStep: AppStep
additionalBeforeAddSubscriptionStep?: AppStep
additionalAfterAddSubscriptionStep?: AppStep
}
export interface AppConfigBranding {
name: string
logoUrl: string
supportUrl: string
}
export interface AppConfigConfig {
additionalLocales: string[]
branding: AppConfigBranding
}
export interface AppConfigResponse {
config: AppConfigConfig
platforms: Record<string, AppDefinition[]>
}
export const adminAppsApi = {
// Get full app config
getConfig: async (): Promise<AppConfigResponse> => {
const response = await apiClient.get<AppConfigResponse>('/cabinet/admin/apps')
return response.data
},
// Get available platforms
getPlatforms: async (): Promise<string[]> => {
const response = await apiClient.get<string[]>('/cabinet/admin/apps/platforms')
return response.data
},
// Get apps for a platform
getPlatformApps: async (platform: string): Promise<AppDefinition[]> => {
const response = await apiClient.get<AppDefinition[]>(`/cabinet/admin/apps/platforms/${platform}`)
return response.data
},
// Create a new app
createApp: async (platform: string, app: AppDefinition): Promise<AppDefinition> => {
const response = await apiClient.post<AppDefinition>(`/cabinet/admin/apps/platforms/${platform}`, {
platform,
app,
})
return response.data
},
// Update an app
updateApp: async (platform: string, appId: string, app: AppDefinition): Promise<AppDefinition> => {
const response = await apiClient.put<AppDefinition>(`/cabinet/admin/apps/platforms/${platform}/${appId}`, {
app,
})
return response.data
},
// Delete an app
deleteApp: async (platform: string, appId: string): Promise<void> => {
await apiClient.delete(`/cabinet/admin/apps/platforms/${platform}/${appId}`)
},
// Reorder apps
reorderApps: async (platform: string, appIds: string[]): Promise<void> => {
await apiClient.post(`/cabinet/admin/apps/platforms/${platform}/reorder`, {
app_ids: appIds,
})
},
// Copy app to another platform
copyApp: async (platform: string, appId: string, targetPlatform: string): Promise<{ new_id: string }> => {
const response = await apiClient.post<{ new_id: string; target_platform: string }>(
`/cabinet/admin/apps/platforms/${platform}/copy/${appId}?target_platform=${targetPlatform}`
)
return response.data
},
// Get branding
getBranding: async (): Promise<AppConfigBranding> => {
const response = await apiClient.get<AppConfigBranding>('/cabinet/admin/apps/branding')
return response.data
},
// Update branding
updateBranding: async (branding: AppConfigBranding): Promise<AppConfigBranding> => {
const response = await apiClient.put<AppConfigBranding>('/cabinet/admin/apps/branding', {
branding,
})
return response.data
},
}

73
src/api/adminSettings.ts Normal file
View File

@@ -0,0 +1,73 @@
import apiClient from './client'
export interface SettingCategoryRef {
key: string
label: string
}
export interface SettingCategorySummary {
key: string
label: string
description: string
items: number
}
export interface SettingChoice {
value: unknown
label: string
description?: string | null
}
export interface SettingHint {
description: string
format: string
example: string
warning: string
}
export interface SettingDefinition {
key: string
name: string
category: SettingCategoryRef
type: string
is_optional: boolean
current: unknown
original: unknown
has_override: boolean
read_only: boolean
choices: SettingChoice[]
hint?: SettingHint | null
}
export const adminSettingsApi = {
// Get list of setting categories
getCategories: async (): Promise<SettingCategorySummary[]> => {
const response = await apiClient.get<SettingCategorySummary[]>('/cabinet/admin/settings/categories')
return response.data
},
// Get all settings or settings for a specific category
getSettings: async (categoryKey?: string): Promise<SettingDefinition[]> => {
const params = categoryKey ? { category_key: categoryKey } : {}
const response = await apiClient.get<SettingDefinition[]>('/cabinet/admin/settings', { params })
return response.data
},
// Get a specific setting by key
getSetting: async (key: string): Promise<SettingDefinition> => {
const response = await apiClient.get<SettingDefinition>(`/cabinet/admin/settings/${key}`)
return response.data
},
// Update a setting value
updateSetting: async (key: string, value: unknown): Promise<SettingDefinition> => {
const response = await apiClient.put<SettingDefinition>(`/cabinet/admin/settings/${key}`, { value })
return response.data
},
// Reset a setting to default
resetSetting: async (key: string): Promise<SettingDefinition> => {
const response = await apiClient.delete<SettingDefinition>(`/cabinet/admin/settings/${key}`)
return response.data
},
}

92
src/api/auth.ts Normal file
View File

@@ -0,0 +1,92 @@
import apiClient from './client'
import type { AuthResponse, TokenResponse, User } from '../types'
export const authApi = {
// Telegram WebApp authentication
loginTelegram: async (initData: string): Promise<AuthResponse> => {
const response = await apiClient.post<AuthResponse>('/cabinet/auth/telegram', {
init_data: initData,
})
return response.data
},
// Telegram Login Widget authentication
loginTelegramWidget: async (data: {
id: number
first_name: string
last_name?: string
username?: string
photo_url?: string
auth_date: number
hash: string
}): Promise<AuthResponse> => {
const response = await apiClient.post<AuthResponse>('/cabinet/auth/telegram/widget', data)
return response.data
},
// Email login
loginEmail: async (email: string, password: string): Promise<AuthResponse> => {
const response = await apiClient.post<AuthResponse>('/cabinet/auth/email/login', {
email,
password,
})
return response.data
},
// Register email (link to existing Telegram account)
registerEmail: async (email: string, password: string): Promise<{ message: string; email: string }> => {
const response = await apiClient.post('/cabinet/auth/email/register', {
email,
password,
})
return response.data
},
// Verify email
verifyEmail: async (token: string): Promise<{ message: string }> => {
const response = await apiClient.post('/cabinet/auth/email/verify', { token })
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,
})
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,
password,
})
return response.data
},
// Get current user
getMe: async (): Promise<User> => {
const response = await apiClient.get<User>('/cabinet/auth/me')
return response.data
},
}

69
src/api/balance.ts Normal file
View File

@@ -0,0 +1,69 @@
import apiClient from './client'
import type { Balance, Transaction, PaymentMethod, PaginatedResponse } from '../types'
export const balanceApi = {
// Get current balance
getBalance: async (): Promise<Balance> => {
const response = await apiClient.get<Balance>('/cabinet/balance')
return response.data
},
// Get transaction history
getTransactions: async (params?: {
page?: number
per_page?: number
type?: string
}): Promise<PaginatedResponse<Transaction>> => {
const response = await apiClient.get<PaginatedResponse<Transaction>>('/cabinet/balance/transactions', {
params,
})
return response.data
},
// Get available payment methods
getPaymentMethods: async (): Promise<PaymentMethod[]> => {
const response = await apiClient.get<PaymentMethod[]>('/cabinet/balance/payment-methods')
return response.data
},
// Create top-up payment
createTopUp: async (amountKopeks: number, paymentMethod: string): Promise<{
payment_id: string
payment_url: string
amount_kopeks: number
amount_rubles: number
status: string
expires_at: string | null
}> => {
const response = await apiClient.post('/cabinet/balance/topup', {
amount_kopeks: amountKopeks,
payment_method: paymentMethod,
})
return response.data
},
// Activate promo code
activatePromocode: async (code: string): Promise<{
success: boolean
message: string
balance_before: number
balance_after: number
bonus_description: string | null
}> => {
const response = await apiClient.post('/cabinet/promocode/activate', { code })
return response.data
},
// Create Telegram Stars invoice for Mini App balance top-up
createStarsInvoice: async (amountKopeks: number): Promise<{
invoice_url: string
stars_amount?: number
amount_kopeks?: number
}> => {
const response = await apiClient.post('/cabinet/balance/stars-invoice', {
amount_kopeks: amountKopeks,
})
return response.data
},
}

48
src/api/branding.ts Normal file
View File

@@ -0,0 +1,48 @@
import apiClient from './client'
export interface BrandingInfo {
name: string
logo_url: string | null
logo_letter: string
has_custom_logo: boolean
}
export const brandingApi = {
// Get current branding (public, no auth required)
getBranding: async (): Promise<BrandingInfo> => {
const response = await apiClient.get<BrandingInfo>('/cabinet/branding')
return response.data
},
// Update project name (admin only)
updateName: async (name: string): Promise<BrandingInfo> => {
const response = await apiClient.put<BrandingInfo>('/cabinet/branding/name', { name })
return response.data
},
// Upload custom logo (admin only)
uploadLogo: async (file: File): Promise<BrandingInfo> => {
const formData = new FormData()
formData.append('file', file)
const response = await apiClient.post<BrandingInfo>('/cabinet/branding/logo', formData, {
headers: {
'Content-Type': 'multipart/form-data',
},
})
return response.data
},
// Delete custom logo (admin only)
deleteLogo: async (): Promise<BrandingInfo> => {
const response = await apiClient.delete<BrandingInfo>('/cabinet/branding/logo')
return response.data
},
// Get logo URL (without cache busting - server handles caching via Cache-Control headers)
getLogoUrl: (branding: BrandingInfo): string | null => {
if (!branding.has_custom_logo || !branding.logo_url) {
return null
}
return `${import.meta.env.VITE_API_URL || ''}${branding.logo_url}`
},
}

88
src/api/client.ts Normal file
View File

@@ -0,0 +1,88 @@
import axios, { AxiosError, InternalAxiosRequestConfig } from 'axios'
const API_BASE_URL = import.meta.env.VITE_API_URL || '/api'
const TELEGRAM_INIT_STORAGE_KEY = 'telegram_init_data'
const getTelegramInitData = (): string | null => {
if (typeof window === 'undefined') return null
const initData = window.Telegram?.WebApp?.initData
if (initData) {
try {
localStorage.setItem(TELEGRAM_INIT_STORAGE_KEY, initData)
} catch {
/* ignore storage errors */
}
return initData
}
try {
return localStorage.getItem(TELEGRAM_INIT_STORAGE_KEY)
} catch {
return null
}
}
export const apiClient = axios.create({
baseURL: API_BASE_URL,
headers: {
'Content-Type': 'application/json',
},
})
// Request interceptor - add auth token
apiClient.interceptors.request.use((config: InternalAxiosRequestConfig) => {
const token = localStorage.getItem('access_token')
if (token && config.headers) {
config.headers.Authorization = `Bearer ${token}`
}
const telegramInitData = getTelegramInitData()
if (telegramInitData && config.headers) {
config.headers['X-Telegram-Init-Data'] = telegramInitData
}
return config
})
// Response interceptor - handle 401, refresh token
apiClient.interceptors.response.use(
(response) => response,
async (error: AxiosError) => {
const originalRequest = error.config as InternalAxiosRequestConfig & { _retry?: boolean }
if (error.response?.status === 401 && !originalRequest._retry) {
originalRequest._retry = true
const refreshToken = localStorage.getItem('refresh_token')
if (refreshToken) {
try {
const response = await axios.post(`${API_BASE_URL}/cabinet/auth/refresh`, {
refresh_token: refreshToken,
})
const { access_token } = response.data
localStorage.setItem('access_token', access_token)
if (originalRequest.headers) {
originalRequest.headers.Authorization = `Bearer ${access_token}`
}
return apiClient(originalRequest)
} catch (refreshError) {
// Refresh failed, clear tokens and redirect to login
localStorage.removeItem('access_token')
localStorage.removeItem('refresh_token')
localStorage.removeItem('user')
window.location.href = '/login'
return Promise.reject(refreshError)
}
}
}
return Promise.reject(error)
}
)
export default apiClient

57
src/api/contests.ts Normal file
View File

@@ -0,0 +1,57 @@
import apiClient from './client'
export interface ContestInfo {
id: number
slug: string
name: string
description: string | null
prize_days: number
is_available: boolean
already_played: boolean
}
export interface ContestGameData {
round_id: number
game_type: string
game_data: Record<string, any>
instructions: string
}
export interface ContestResult {
is_winner: boolean
message: string
prize_days?: number
}
export interface ContestsCountResponse {
count: number
}
export const contestsApi = {
// Get count of available contests
getCount: async (): Promise<ContestsCountResponse> => {
const response = await apiClient.get<ContestsCountResponse>('/cabinet/contests/count')
return response.data
},
// Get list of available contests
getContests: async (): Promise<ContestInfo[]> => {
const response = await apiClient.get<ContestInfo[]>('/cabinet/contests')
return response.data
},
// Get game data for a specific contest
getContestGame: async (roundId: number): Promise<ContestGameData> => {
const response = await apiClient.get<ContestGameData>(`/cabinet/contests/${roundId}`)
return response.data
},
// Submit answer for a contest
submitAnswer: async (roundId: number, answer: string): Promise<ContestResult> => {
const response = await apiClient.post<ContestResult>(`/cabinet/contests/${roundId}/answer`, {
round_id: roundId,
answer,
})
return response.data
},
}

98
src/api/currency.ts Normal file
View File

@@ -0,0 +1,98 @@
// Currency exchange rate API
// Uses free exchangerate.host API
interface ExchangeRates {
USD: number
CNY: number
IRR: number
}
interface CachedRates {
rates: ExchangeRates
timestamp: number
}
const CACHE_DURATION = 60 * 60 * 1000 // 1 hour in milliseconds
// Fallback rates if API fails (approximate)
const FALLBACK_RATES: ExchangeRates = {
USD: 100, // 1 USD = 100 RUB
CNY: 14, // 1 CNY = 14 RUB
IRR: 0.0024, // 1 IRR = 0.0024 RUB (or 1 RUB = ~420 IRR)
}
let cachedRates: CachedRates | null = null
export const currencyApi = {
// Get all exchange rates (RUB to other currencies)
getExchangeRates: async (): Promise<ExchangeRates> => {
// Check cache first
if (cachedRates && Date.now() - cachedRates.timestamp < CACHE_DURATION) {
return cachedRates.rates
}
try {
// Try primary API (exchangerate.host) - get RUB based rates
const response = await fetch(
'https://api.exchangerate.host/latest?base=RUB&symbols=USD,CNY,IRR'
)
if (response.ok) {
const data = await response.json()
if (data.success && data.rates) {
// API returns how much of each currency equals 1 RUB
// We need the inverse: how many RUB equals 1 of each currency
const rates: ExchangeRates = {
USD: data.rates.USD ? 1 / data.rates.USD : FALLBACK_RATES.USD,
CNY: data.rates.CNY ? 1 / data.rates.CNY : FALLBACK_RATES.CNY,
IRR: data.rates.IRR ? 1 / data.rates.IRR : FALLBACK_RATES.IRR,
}
cachedRates = { rates, timestamp: Date.now() }
return rates
}
}
// Try backup API (open.er-api.com)
const backupResponse = await fetch('https://open.er-api.com/v6/latest/RUB')
if (backupResponse.ok) {
const backupData = await backupResponse.json()
if (backupData.rates) {
const rates: ExchangeRates = {
USD: backupData.rates.USD ? 1 / backupData.rates.USD : FALLBACK_RATES.USD,
CNY: backupData.rates.CNY ? 1 / backupData.rates.CNY : FALLBACK_RATES.CNY,
IRR: backupData.rates.IRR ? 1 / backupData.rates.IRR : FALLBACK_RATES.IRR,
}
cachedRates = { rates, timestamp: Date.now() }
return rates
}
}
// Return fallback rates if both APIs fail
return FALLBACK_RATES
} catch (error) {
console.warn('Failed to fetch exchange rates, using fallback:', error)
return FALLBACK_RATES
}
},
// Convert RUB to target currency
convertFromRub: (rubAmount: number, targetCurrency: keyof ExchangeRates, rates: ExchangeRates): number => {
const rate = rates[targetCurrency]
if (!rate || rate <= 0) {
return rubAmount / FALLBACK_RATES[targetCurrency]
}
return rubAmount / rate
},
// Convert from target currency to RUB
convertToRub: (amount: number, sourceCurrency: keyof ExchangeRates, rates: ExchangeRates): number => {
const rate = rates[sourceCurrency]
if (!rate || rate <= 0) {
return amount * FALLBACK_RATES[sourceCurrency]
}
return amount * rate
},
}
export type { ExchangeRates }

12
src/api/index.ts Normal file
View File

@@ -0,0 +1,12 @@
export { apiClient } from './client'
export { authApi } from './auth'
export { subscriptionApi } from './subscription'
export { balanceApi } from './balance'
export { referralApi } from './referral'
export { ticketsApi } from './tickets'
export { contestsApi } from './contests'
export { pollsApi } from './polls'
export { promoApi } from './promo'
export { notificationsApi } from './notifications'
export { infoApi } from './info'
export { adminSettingsApi } from './adminSettings'

102
src/api/info.ts Normal file
View File

@@ -0,0 +1,102 @@
import apiClient from './client'
import type { SupportConfig } from '../types'
export interface FaqPage {
id: number
title: string
content: string
order: number
}
export interface RulesResponse {
content: string
updated_at: string | null
}
export interface PrivacyPolicyResponse {
content: string
updated_at: string | null
}
export interface PublicOfferResponse {
content: string
updated_at: string | null
}
export interface ServiceInfo {
name: string
description: string | null
support_email: string | null
support_telegram: string | null
website: string | null
}
export interface LanguageInfo {
code: string
name: string
flag: string
}
export const infoApi = {
// Get FAQ pages list
getFaqPages: async (): Promise<FaqPage[]> => {
const response = await apiClient.get<FaqPage[]>('/cabinet/info/faq')
return response.data
},
// Get specific FAQ page
getFaqPage: async (pageId: number): Promise<FaqPage> => {
const response = await apiClient.get<FaqPage>(`/cabinet/info/faq/${pageId}`)
return response.data
},
// Get service rules
getRules: async (): Promise<RulesResponse> => {
const response = await apiClient.get<RulesResponse>('/cabinet/info/rules')
return response.data
},
// Get privacy policy
getPrivacyPolicy: async (): Promise<PrivacyPolicyResponse> => {
const response = await apiClient.get<PrivacyPolicyResponse>('/cabinet/info/privacy-policy')
return response.data
},
// Get public offer
getPublicOffer: async (): Promise<PublicOfferResponse> => {
const response = await apiClient.get<PublicOfferResponse>('/cabinet/info/public-offer')
return response.data
},
// Get service info
getServiceInfo: async (): Promise<ServiceInfo> => {
const response = await apiClient.get<ServiceInfo>('/cabinet/info/service')
return response.data
},
// Get available languages
getLanguages: async (): Promise<{ languages: LanguageInfo[]; default: string }> => {
const response = await apiClient.get('/cabinet/info/languages')
return response.data
},
// Get user language
getUserLanguage: async (): Promise<{ language: string }> => {
const response = await apiClient.get<{ language: string }>('/cabinet/info/user/language')
return response.data
},
// Update user language
updateUserLanguage: async (language: string): Promise<{ language: string }> => {
const response = await apiClient.patch<{ language: string }>('/cabinet/info/user/language', {
language,
})
return response.data
},
// Get support configuration
getSupportConfig: async (): Promise<SupportConfig> => {
const response = await apiClient.get<SupportConfig>('/cabinet/info/support-config')
return response.data
},
}

36
src/api/miniapp.ts Normal file
View File

@@ -0,0 +1,36 @@
export interface MiniappCreatePaymentPayload {
initData: string
method: string
amountKopeks?: number | null
option?: string | null
}
export interface MiniappCreatePaymentResponse {
payment_url: string
amount_kopeks?: number
extra?: Record<string, unknown>
}
export const miniappApi = {
// Create payment inside Telegram Mini App (same flow as miniapp/index.html)
createPayment: async (
payload: MiniappCreatePaymentPayload
): Promise<MiniappCreatePaymentResponse> => {
const res = await fetch('/miniapp/payments/create', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
initData: payload.initData || '',
method: payload.method,
amountKopeks: payload.amountKopeks ?? null,
option: payload.option ?? null,
}),
})
const data = await res.json().catch(() => ({}))
if (!res.ok) {
const message = (data && (data.detail || data.message)) || 'Failed to create payment'
throw new Error(String(message))
}
return data as MiniappCreatePaymentResponse
},
}

58
src/api/notifications.ts Normal file
View File

@@ -0,0 +1,58 @@
import apiClient from './client'
export interface NotificationSettings {
subscription_expiry_enabled: boolean
subscription_expiry_days: number
traffic_warning_enabled: boolean
traffic_warning_percent: number
balance_low_enabled: boolean
balance_low_threshold: number
news_enabled: boolean
promo_offers_enabled: boolean
}
export interface NotificationSettingsUpdate {
subscription_expiry_enabled?: boolean
subscription_expiry_days?: number
traffic_warning_enabled?: boolean
traffic_warning_percent?: number
balance_low_enabled?: boolean
balance_low_threshold?: number
news_enabled?: boolean
promo_offers_enabled?: boolean
}
export const notificationsApi = {
// Get notification settings
getSettings: async (): Promise<NotificationSettings> => {
const response = await apiClient.get<NotificationSettings>('/cabinet/notifications')
return response.data
},
// Update notification settings
updateSettings: async (settings: NotificationSettingsUpdate): Promise<NotificationSettings> => {
const response = await apiClient.patch<NotificationSettings>('/cabinet/notifications', settings)
return response.data
},
// Send test notification
sendTestNotification: async (): Promise<{ success: boolean; message: string }> => {
const response = await apiClient.post<{ success: boolean; message: string }>(
'/cabinet/notifications/test'
)
return response.data
},
// Get notification history
getHistory: async (limit = 20, offset = 0): Promise<{
notifications: any[]
total: number
limit: number
offset: number
}> => {
const response = await apiClient.get('/cabinet/notifications/history', {
params: { limit, offset },
})
return response.data
},
}

85
src/api/polls.ts Normal file
View File

@@ -0,0 +1,85 @@
import apiClient from './client'
export interface PollOption {
id: number
text: string
order: number
}
export interface PollQuestion {
id: number
text: string
order: number
options: PollOption[]
}
export interface PollInfo {
id: number
response_id: number
title: string
description: string | null
total_questions: number
answered_questions: number
is_completed: boolean
reward_amount: number | null
}
export interface PollStartResponse {
response_id: number
current_question_index: number
total_questions: number
question: PollQuestion
}
export interface PollAnswerResponse {
success: boolean
is_completed: boolean
next_question: PollQuestion | null
current_question_index: number | null
total_questions: number
reward_granted: number | null
message: string | null
}
export interface PollsCountResponse {
count: number
}
export const pollsApi = {
// Get count of available polls
getCount: async (): Promise<PollsCountResponse> => {
const response = await apiClient.get<PollsCountResponse>('/cabinet/polls/count')
return response.data
},
// Get available polls
getPolls: async (): Promise<PollInfo[]> => {
const response = await apiClient.get<PollInfo[]>('/cabinet/polls')
return response.data
},
// Get poll details
getPollDetails: async (responseId: number): Promise<PollInfo> => {
const response = await apiClient.get<PollInfo>(`/cabinet/polls/${responseId}`)
return response.data
},
// Start or continue poll
startPoll: async (responseId: number): Promise<PollStartResponse> => {
const response = await apiClient.post<PollStartResponse>(`/cabinet/polls/${responseId}/start`)
return response.data
},
// Answer a question
answerQuestion: async (
responseId: number,
questionId: number,
optionId: number
): Promise<PollAnswerResponse> => {
const response = await apiClient.post<PollAnswerResponse>(
`/cabinet/polls/${responseId}/questions/${questionId}/answer`,
{ option_id: optionId }
)
return response.data
},
}

69
src/api/promo.ts Normal file
View File

@@ -0,0 +1,69 @@
import apiClient from './client'
export interface PromoOffer {
id: number
notification_type: string
discount_percent: number | null
effect_type: string
expires_at: string
is_active: boolean
is_claimed: boolean
claimed_at: string | null
extra_data: Record<string, any> | null
}
export interface ActiveDiscount {
discount_percent: number
source: string | null
expires_at: string | null
is_active: boolean
}
export interface PromoGroupDiscounts {
group_name: string | null
server_discount_percent: number
traffic_discount_percent: number
device_discount_percent: number
period_discounts: Record<string, number>
}
export interface ClaimOfferResponse {
success: boolean
message: string
discount_percent: number | null
expires_at: string | null
}
export const promoApi = {
// Get available promo offers
getOffers: async (): Promise<PromoOffer[]> => {
const response = await apiClient.get<PromoOffer[]>('/cabinet/promo/offers')
return response.data
},
// Get active discount
getActiveDiscount: async (): Promise<ActiveDiscount> => {
const response = await apiClient.get<ActiveDiscount>('/cabinet/promo/active-discount')
return response.data
},
// Get promo group discounts
getGroupDiscounts: async (): Promise<PromoGroupDiscounts> => {
const response = await apiClient.get<PromoGroupDiscounts>('/cabinet/promo/group-discounts')
return response.data
},
// Claim a promo offer
claimOffer: async (offerId: number): Promise<ClaimOfferResponse> => {
const response = await apiClient.post<ClaimOfferResponse>('/cabinet/promo/claim', {
offer_id: offerId,
})
return response.data
},
// Clear active discount
clearActiveDiscount: async (): Promise<{ message: string }> => {
const response = await apiClient.delete<{ message: string }>('/cabinet/promo/active-discount')
return response.data
},
}

62
src/api/referral.ts Normal file
View File

@@ -0,0 +1,62 @@
import apiClient from './client'
import type { ReferralInfo, ReferralTerms, PaginatedResponse } from '../types'
interface ReferralItem {
id: number
username: string | null
first_name: string | null
created_at: string
has_subscription: boolean
has_paid: boolean
}
interface ReferralEarning {
id: number
amount_kopeks: number
amount_rubles: number
reason: string
referral_username: string | null
referral_first_name: string | null
created_at: string
}
interface ReferralEarningsList extends PaginatedResponse<ReferralEarning> {
total_amount_kopeks: number
total_amount_rubles: number
}
export const referralApi = {
// Get referral info
getReferralInfo: async (): Promise<ReferralInfo> => {
const response = await apiClient.get<ReferralInfo>('/cabinet/referral')
return response.data
},
// Get referral list
getReferralList: async (params?: {
page?: number
per_page?: number
}): Promise<PaginatedResponse<ReferralItem>> => {
const response = await apiClient.get<PaginatedResponse<ReferralItem>>('/cabinet/referral/list', {
params,
})
return response.data
},
// Get referral earnings
getReferralEarnings: async (params?: {
page?: number
per_page?: number
}): Promise<ReferralEarningsList> => {
const response = await apiClient.get<ReferralEarningsList>('/cabinet/referral/earnings', {
params,
})
return response.data
},
// Get referral terms
getReferralTerms: async (): Promise<ReferralTerms> => {
const response = await apiClient.get<ReferralTerms>('/cabinet/referral/terms')
return response.data
},
}

142
src/api/servers.ts Normal file
View File

@@ -0,0 +1,142 @@
import apiClient from './client'
// Types
export interface PromoGroupInfo {
id: number
name: string
is_selected: boolean
}
export interface ServerListItem {
id: number
squad_uuid: string
display_name: string
original_name: string | null
country_code: string | null
is_available: boolean
is_trial_eligible: boolean
price_kopeks: number
price_rubles: number
max_users: number | null
current_users: number
sort_order: number
is_full: boolean
availability_status: string
created_at: string
}
export interface ServerListResponse {
servers: ServerListItem[]
total: number
}
export interface ServerDetail {
id: number
squad_uuid: string
display_name: string
original_name: string | null
country_code: string | null
description: string | null
is_available: boolean
is_trial_eligible: boolean
price_kopeks: number
price_rubles: number
max_users: number | null
current_users: number
sort_order: number
is_full: boolean
availability_status: string
promo_groups: PromoGroupInfo[]
active_subscriptions: number
tariffs_using: string[]
created_at: string
updated_at: string | null
}
export interface ServerUpdateRequest {
display_name?: string
description?: string
country_code?: string
is_available?: boolean
is_trial_eligible?: boolean
price_kopeks?: number
max_users?: number
sort_order?: number
promo_group_ids?: number[]
}
export interface ServerToggleResponse {
id: number
is_available: boolean
message: string
}
export interface ServerTrialToggleResponse {
id: number
is_trial_eligible: boolean
message: string
}
export interface ServerStats {
id: number
display_name: string
squad_uuid: string
current_users: number
max_users: number | null
active_subscriptions: number
trial_subscriptions: number
usage_percent: number | null
}
export interface ServerSyncResponse {
created: number
updated: number
removed: number
message: string
}
export const serversApi = {
// Get all servers
getServers: async (includeUnavailable = true): Promise<ServerListResponse> => {
const response = await apiClient.get('/cabinet/admin/servers', {
params: { include_unavailable: includeUnavailable }
})
return response.data
},
// Get single server
getServer: async (serverId: number): Promise<ServerDetail> => {
const response = await apiClient.get(`/cabinet/admin/servers/${serverId}`)
return response.data
},
// Update server
updateServer: async (serverId: number, data: ServerUpdateRequest): Promise<ServerDetail> => {
const response = await apiClient.put(`/cabinet/admin/servers/${serverId}`, data)
return response.data
},
// Toggle server availability
toggleServer: async (serverId: number): Promise<ServerToggleResponse> => {
const response = await apiClient.post(`/cabinet/admin/servers/${serverId}/toggle`)
return response.data
},
// Toggle trial eligibility
toggleTrial: async (serverId: number): Promise<ServerTrialToggleResponse> => {
const response = await apiClient.post(`/cabinet/admin/servers/${serverId}/trial`)
return response.data
},
// Get server stats
getServerStats: async (serverId: number): Promise<ServerStats> => {
const response = await apiClient.get(`/cabinet/admin/servers/${serverId}/stats`)
return response.data
},
// Sync servers with RemnaWave
syncServers: async (): Promise<ServerSyncResponse> => {
const response = await apiClient.post('/cabinet/admin/servers/sync')
return response.data
},
}

324
src/api/subscription.ts Normal file
View File

@@ -0,0 +1,324 @@
import apiClient from './client'
import type { Subscription, RenewalOption, TrafficPackage, TrialInfo, PurchaseOptions, PurchaseSelection, PurchasePreview, AppConfig } from '../types'
export const subscriptionApi = {
// Get current subscription
getSubscription: async (): Promise<Subscription> => {
const response = await apiClient.get<Subscription>('/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<{
message: string
new_end_date: string
amount_paid_kopeks: number
}> => {
const response = await apiClient.post('/cabinet/subscription/renew', {
period_days: periodDays,
})
return response.data
},
// Get traffic packages
getTrafficPackages: async (): Promise<TrafficPackage[]> => {
const response = await apiClient.get<TrafficPackage[]>('/cabinet/subscription/traffic-packages')
return response.data
},
// Purchase traffic
purchaseTraffic: async (gb: number): Promise<{
message: string
gb_added: number
amount_paid_kopeks: number
}> => {
const response = await apiClient.post('/cabinet/subscription/traffic', { gb })
return response.data
},
// Purchase devices
purchaseDevices: async (devices: number): Promise<{
success: boolean
message: string
devices_added: number
new_device_limit: number
price_kopeks: number
price_label: string
balance_kopeks: number
balance_label: string
}> => {
const response = await apiClient.post('/cabinet/subscription/devices/purchase', { devices })
return response.data
},
// Get device purchase price
getDevicePrice: async (devices: number = 1): Promise<{
available: boolean
reason?: string
devices?: number
price_per_device_kopeks?: number
price_per_device_label?: string
total_price_kopeks?: number
total_price_label?: string
current_device_limit?: number
days_left?: number
base_device_price_kopeks?: number
}> => {
const response = await apiClient.get('/cabinet/subscription/devices/price', { params: { devices } })
return response.data
},
// Update autopay settings
updateAutopay: async (enabled: boolean, daysBefore?: number): Promise<{
message: string
autopay_enabled: boolean
autopay_days_before: number
}> => {
const response = await apiClient.patch('/cabinet/subscription/autopay', {
enabled,
days_before: daysBefore,
})
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', {
selection,
})
return response.data
},
// Submit purchase
submitPurchase: async (selection: PurchaseSelection): Promise<{
success: boolean
message: string
subscription: Subscription
was_trial_conversion: boolean
}> => {
const response = await apiClient.post('/cabinet/subscription/purchase', {
selection,
})
return response.data
},
// Purchase tariff (for tariffs mode)
purchaseTariff: async (tariffId: number, periodDays: number, trafficGb?: number): Promise<{
success: boolean
message: string
subscription: Subscription
tariff_id: number
tariff_name: string
balance_kopeks: number
balance_label: string
}> => {
const response = await apiClient.post('/cabinet/subscription/purchase-tariff', {
tariff_id: tariffId,
period_days: periodDays,
traffic_gb: trafficGb,
})
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
name: string
country_code: string | null
price_kopeks: number
price_rubles: number
is_available: boolean
is_connected: boolean
is_trial_eligible: boolean
}>
connected_count: number
has_subscription: boolean
}> => {
const response = await apiClient.get('/cabinet/subscription/countries')
return response.data
},
// Update countries/servers
updateCountries: async (countries: string[]): Promise<{
message: string
added: string[]
removed: string[]
amount_paid_kopeks: number
connected_squads: string[]
}> => {
const response = await apiClient.post('/cabinet/subscription/countries', { countries })
return response.data
},
// Get connection link and instructions
getConnectionLink: async (): Promise<{
subscription_url: string | null
display_link: string | null
happ_redirect_link: string | null
happ_scheme_link: string | null
connect_mode: string
hide_link: boolean
instructions: {
steps: string[]
}
}> => {
const response = await apiClient.get('/cabinet/subscription/connection-link')
return response.data
},
// Get hApp download links
getHappDownloads: async (): Promise<{
platforms: Record<string, {
name: string
icon: string
link: string
}>
happ_enabled: boolean
}> => {
const response = await apiClient.get('/cabinet/subscription/happ-downloads')
return response.data
},
// ============ Device Management ============
// Get connected devices
getDevices: async (): Promise<{
devices: Array<{
hwid: string
platform: string
device_model: string
created_at: string | null
}>
total: number
device_limit: number
}> => {
const response = await apiClient.get('/cabinet/subscription/devices')
return response.data
},
// Delete a specific device
deleteDevice: async (hwid: string): Promise<{
success: boolean
message: string
deleted_hwid: string
}> => {
const response = await apiClient.delete(`/cabinet/subscription/devices/${encodeURIComponent(hwid)}`)
return response.data
},
// Delete all devices
deleteAllDevices: async (): Promise<{
success: boolean
message: string
deleted_count: number
}> => {
const response = await apiClient.delete('/cabinet/subscription/devices')
return response.data
},
// ============ Tariff Switch ============
// Preview tariff switch cost
previewTariffSwitch: async (tariffId: number): Promise<{
can_switch: boolean
current_tariff_id: number | null
current_tariff_name: string | null
new_tariff_id: number
new_tariff_name: string
remaining_days: number
upgrade_cost_kopeks: number
upgrade_cost_label: string
balance_kopeks: number
balance_label: string
has_enough_balance: boolean
missing_amount_kopeks: number
missing_amount_label: string
is_upgrade: boolean
}> => {
const response = await apiClient.post('/cabinet/subscription/tariff/switch/preview', {
tariff_id: tariffId,
period_days: 30, // Default period for switch
})
return response.data
},
// Switch to a different tariff
switchTariff: async (tariffId: number): Promise<{
success: boolean
message: string
subscription: Subscription
old_tariff_name: string
new_tariff_id: number
new_tariff_name: string
charged_kopeks: number
balance_kopeks: number
balance_label: string
}> => {
const response = await apiClient.post('/cabinet/subscription/tariff/switch', {
tariff_id: tariffId,
period_days: 30,
})
return response.data
},
// ============ Subscription Pause (Daily Tariffs) ============
// Toggle pause/resume for daily subscription
togglePause: async (): Promise<{
success: boolean
message: string
is_paused: boolean
balance_kopeks: number
balance_label: string
}> => {
const response = await apiClient.post('/cabinet/subscription/pause')
return response.data
},
// ============ Traffic Switch ============
// Switch to a different traffic package
switchTraffic: async (gb: number): Promise<{
success: boolean
message: string
old_traffic_gb: number
new_traffic_gb: number
charged_kopeks: number
balance_kopeks: number
balance_label: string
}> => {
const response = await apiClient.put('/cabinet/subscription/traffic', { gb })
return response.data
},
}

228
src/api/tariffs.ts Normal file
View File

@@ -0,0 +1,228 @@
import apiClient from './client'
// Types
export interface PeriodPrice {
days: number
price_kopeks: number
price_rubles?: number
}
export interface ServerTrafficLimit {
traffic_limit_gb: number
}
export interface ServerInfo {
id: number
squad_uuid: string
display_name: string
country_code: string | null
is_selected: boolean
traffic_limit_gb?: number | null
}
export interface PromoGroupInfo {
id: number
name: string
is_selected: boolean
}
export interface TariffListItem {
id: number
name: string
description: string | null
is_active: boolean
is_trial_available: boolean
traffic_limit_gb: number
device_limit: number
tier_level: number
display_order: number
servers_count: number
subscriptions_count: number
created_at: string
}
export interface TariffListResponse {
tariffs: TariffListItem[]
total: number
}
export interface TariffDetail {
id: number
name: string
description: string | null
is_active: boolean
is_trial_available: boolean
traffic_limit_gb: number
device_limit: number
device_price_kopeks: number | null
tier_level: number
display_order: number
period_prices: PeriodPrice[]
allowed_squads: string[]
server_traffic_limits: Record<string, ServerTrafficLimit>
servers: ServerInfo[]
promo_groups: PromoGroupInfo[]
subscriptions_count: number
// Произвольное количество дней
custom_days_enabled: boolean
price_per_day_kopeks: number
min_days: number
max_days: number
// Произвольный трафик при покупке
custom_traffic_enabled: boolean
traffic_price_per_gb_kopeks: number
min_traffic_gb: number
max_traffic_gb: number
// Докупка трафика
traffic_topup_enabled: boolean
traffic_topup_packages: Record<string, number>
max_topup_traffic_gb: number
// Дневной тариф
is_daily: boolean
daily_price_kopeks: number
created_at: string
updated_at: string | null
}
export interface TariffCreateRequest {
name: string
description?: string
is_active?: boolean
traffic_limit_gb?: number
device_limit?: number
device_price_kopeks?: number
tier_level?: number
period_prices?: PeriodPrice[]
allowed_squads?: string[]
server_traffic_limits?: Record<string, ServerTrafficLimit>
promo_group_ids?: number[]
// Произвольное количество дней
custom_days_enabled?: boolean
price_per_day_kopeks?: number
min_days?: number
max_days?: number
// Произвольный трафик при покупке
custom_traffic_enabled?: boolean
traffic_price_per_gb_kopeks?: number
min_traffic_gb?: number
max_traffic_gb?: number
// Докупка трафика
traffic_topup_enabled?: boolean
traffic_topup_packages?: Record<string, number>
max_topup_traffic_gb?: number
// Дневной тариф
is_daily?: boolean
daily_price_kopeks?: number
}
export interface TariffUpdateRequest {
name?: string
description?: string
is_active?: boolean
traffic_limit_gb?: number
device_limit?: number
device_price_kopeks?: number
tier_level?: number
display_order?: number
period_prices?: PeriodPrice[]
allowed_squads?: string[]
server_traffic_limits?: Record<string, ServerTrafficLimit>
promo_group_ids?: number[]
// Произвольное количество дней
custom_days_enabled?: boolean
price_per_day_kopeks?: number
min_days?: number
max_days?: number
// Произвольный трафик при покупке
custom_traffic_enabled?: boolean
traffic_price_per_gb_kopeks?: number
min_traffic_gb?: number
max_traffic_gb?: number
// Докупка трафика
traffic_topup_enabled?: boolean
traffic_topup_packages?: Record<string, number>
max_topup_traffic_gb?: number
// Дневной тариф
is_daily?: boolean
daily_price_kopeks?: number
}
export interface TariffToggleResponse {
id: number
is_active: boolean
message: string
}
export interface TariffTrialResponse {
id: number
is_trial_available: boolean
message: string
}
export interface TariffStats {
id: number
name: string
subscriptions_count: number
active_subscriptions: number
trial_subscriptions: number
revenue_kopeks: number
revenue_rubles: number
}
export const tariffsApi = {
// Get all tariffs
getTariffs: async (includeInactive = true): Promise<TariffListResponse> => {
const response = await apiClient.get('/cabinet/admin/tariffs', {
params: { include_inactive: includeInactive }
})
return response.data
},
// Get single tariff
getTariff: async (tariffId: number): Promise<TariffDetail> => {
const response = await apiClient.get(`/cabinet/admin/tariffs/${tariffId}`)
return response.data
},
// Create new tariff
createTariff: async (data: TariffCreateRequest): Promise<TariffDetail> => {
const response = await apiClient.post('/cabinet/admin/tariffs', data)
return response.data
},
// Update tariff
updateTariff: async (tariffId: number, data: TariffUpdateRequest): Promise<TariffDetail> => {
const response = await apiClient.put(`/cabinet/admin/tariffs/${tariffId}`, data)
return response.data
},
// Delete tariff
deleteTariff: async (tariffId: number): Promise<{ message: string }> => {
const response = await apiClient.delete(`/cabinet/admin/tariffs/${tariffId}`)
return response.data
},
// Toggle tariff active status
toggleTariff: async (tariffId: number): Promise<TariffToggleResponse> => {
const response = await apiClient.post(`/cabinet/admin/tariffs/${tariffId}/toggle`)
return response.data
},
// Toggle trial status
toggleTrial: async (tariffId: number): Promise<TariffTrialResponse> => {
const response = await apiClient.post(`/cabinet/admin/tariffs/${tariffId}/trial`)
return response.data
},
// Get tariff stats
getTariffStats: async (tariffId: number): Promise<TariffStats> => {
const response = await apiClient.get(`/cabinet/admin/tariffs/${tariffId}/stats`)
return response.data
},
// Get available servers for selection
getAvailableServers: async (): Promise<ServerInfo[]> => {
const response = await apiClient.get('/cabinet/admin/tariffs/available-servers')
return response.data
},
}

43
src/api/themeColors.ts Normal file
View File

@@ -0,0 +1,43 @@
import apiClient from './client'
import { ThemeSettings, DEFAULT_THEME_COLORS, EnabledThemes, DEFAULT_ENABLED_THEMES } from '../types/theme'
export const themeColorsApi = {
// Get current theme colors (public, no auth required)
getColors: async (): Promise<ThemeSettings> => {
try {
const response = await apiClient.get<ThemeSettings>('/cabinet/branding/colors')
return response.data
} catch {
// Return default colors if endpoint not available
return DEFAULT_THEME_COLORS
}
},
// Update theme colors (admin only)
updateColors: async (colors: Partial<ThemeSettings>): Promise<ThemeSettings> => {
const response = await apiClient.patch<ThemeSettings>('/cabinet/branding/colors', colors)
return response.data
},
// Reset to default colors (admin only)
resetColors: async (): Promise<ThemeSettings> => {
const response = await apiClient.post<ThemeSettings>('/cabinet/branding/colors/reset')
return response.data
},
// Get enabled themes (public, no auth required)
getEnabledThemes: async (): Promise<EnabledThemes> => {
try {
const response = await apiClient.get<EnabledThemes>('/cabinet/branding/themes')
return response.data
} catch {
return DEFAULT_ENABLED_THEMES
}
},
// Update enabled themes (admin only)
updateEnabledThemes: async (themes: Partial<EnabledThemes>): Promise<EnabledThemes> => {
const response = await apiClient.patch<EnabledThemes>('/cabinet/branding/themes', themes)
return response.data
},
}

84
src/api/tickets.ts Normal file
View File

@@ -0,0 +1,84 @@
import apiClient from './client'
import type { Ticket, TicketDetail, TicketMessage, PaginatedResponse } from '../types'
// Media upload response type
interface MediaUploadResponse {
media_type: string
file_id: string
file_unique_id: string | null
media_url: string
}
// Media parameters for ticket messages
interface MediaParams {
media_type?: string
media_file_id?: string
media_caption?: string
}
export const ticketsApi = {
// Get tickets list
getTickets: async (params?: {
page?: number
per_page?: number
status?: string
}): Promise<PaginatedResponse<Ticket>> => {
const response = await apiClient.get<PaginatedResponse<Ticket>>('/cabinet/tickets', {
params,
})
return response.data
},
// Create ticket with optional media
createTicket: async (
title: string,
message: string,
media?: MediaParams
): Promise<TicketDetail> => {
const response = await apiClient.post<TicketDetail>('/cabinet/tickets', {
title,
message,
...media,
})
return response.data
},
// Get ticket detail
getTicket: async (ticketId: number): Promise<TicketDetail> => {
const response = await apiClient.get<TicketDetail>(`/cabinet/tickets/${ticketId}`)
return response.data
},
// Add message to ticket with optional media
addMessage: async (
ticketId: number,
message: string,
media?: MediaParams
): Promise<TicketMessage> => {
const response = await apiClient.post<TicketMessage>(`/cabinet/tickets/${ticketId}/messages`, {
message,
...media,
})
return response.data
},
// Upload media file for tickets
uploadMedia: async (file: File, mediaType: string = 'photo'): Promise<MediaUploadResponse> => {
const formData = new FormData()
formData.append('file', file)
formData.append('media_type', mediaType)
const response = await apiClient.post<MediaUploadResponse>('/cabinet/media/upload', formData, {
headers: {
'Content-Type': 'multipart/form-data',
},
})
return response.data
},
// Get media URL for display
getMediaUrl: (fileId: string): string => {
const baseUrl = import.meta.env.VITE_API_URL || ''
return `${baseUrl}/cabinet/media/${fileId}`
},
}

281
src/api/wheel.ts Normal file
View File

@@ -0,0 +1,281 @@
import apiClient from './client'
// ==================== TYPES ====================
export interface WheelPrize {
id: number
display_name: string
emoji: string
color: string
prize_type: string
}
export interface WheelConfig {
is_enabled: boolean
name: string
spin_cost_stars: number | null
spin_cost_days: number | null
spin_cost_stars_enabled: boolean
spin_cost_days_enabled: boolean
prizes: WheelPrize[]
daily_limit: number
user_spins_today: number
can_spin: boolean
can_spin_reason: string | null
can_pay_stars: boolean
can_pay_days: boolean
user_balance_kopeks: number
required_balance_kopeks: number
}
export interface SpinAvailability {
can_spin: boolean
reason: string | null
spins_remaining_today: number
can_pay_stars: boolean
can_pay_days: boolean
min_subscription_days: number
user_subscription_days: number
}
export interface SpinResult {
success: boolean
prize_id: number | null
prize_type: string | null
prize_value: number
prize_display_name: string
emoji: string
color: string
rotation_degrees: number
message: string
promocode: string | null
error: string | null
}
export interface SpinHistoryItem {
id: number
payment_type: string
payment_amount: number
prize_type: string
prize_value: number
prize_display_name: string
emoji: string
color: string
prize_value_kopeks: number
created_at: string
}
export interface SpinHistoryResponse {
items: SpinHistoryItem[]
total: number
page: number
per_page: number
pages: number
}
export interface StarsInvoiceResponse {
invoice_url: string
stars_amount: number
}
// Admin types
export interface WheelPrizeAdmin {
id: number
config_id: number
prize_type: string
prize_value: number
display_name: string
emoji: string
color: string
prize_value_kopeks: number
sort_order: number
manual_probability: number | null
is_active: boolean
promo_balance_bonus_kopeks: number
promo_subscription_days: number
promo_traffic_gb: number
created_at: string | null
updated_at: string | null
}
export interface AdminWheelConfig {
id: number
is_enabled: boolean
name: string
spin_cost_stars: number
spin_cost_days: number
spin_cost_stars_enabled: boolean
spin_cost_days_enabled: boolean
rtp_percent: number
daily_spin_limit: number
min_subscription_days_for_day_payment: number
promo_prefix: string
promo_validity_days: number
prizes: WheelPrizeAdmin[]
created_at: string | null
updated_at: string | null
}
export interface WheelStatistics {
total_spins: number
total_revenue_kopeks: number
total_payout_kopeks: number
actual_rtp_percent: number
configured_rtp_percent: number
spins_by_payment_type: Record<string, { count: number; total_kopeks: number }>
prizes_distribution: Array<{
prize_type: string
display_name: string
count: number
total_kopeks: number
}>
top_wins: Array<{
user_id: number
username: string | null
prize_display_name: string
prize_value_kopeks: number
created_at: string | null
}>
period_from: string | null
period_to: string | null
}
export interface AdminSpinItem {
id: number
user_id: number
username: string | null
payment_type: string
payment_amount: number
payment_value_kopeks: number
prize_type: string
prize_value: number
prize_display_name: string
prize_value_kopeks: number
is_applied: boolean
created_at: string
}
export interface AdminSpinsResponse {
items: AdminSpinItem[]
total: number
page: number
per_page: number
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,
})
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 },
})
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: {
prize_type: string
prize_value: number
display_name: string
emoji?: string
color?: string
prize_value_kopeks: number
sort_order?: number
manual_probability?: number | null
is_active?: boolean
promo_balance_bonus_kopeks?: number
promo_subscription_days?: number
promo_traffic_gb?: number
}): 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>): Promise<WheelPrizeAdmin> => {
const response = await apiClient.put<WheelPrizeAdmin>(`/cabinet/admin/wheel/prizes/${prizeId}`, data)
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 },
})
return response.data
},
// Get all spins
getSpins: async (params?: {
user_id?: number
date_from?: string
date_to?: string
page?: number
per_page?: number
}): Promise<AdminSpinsResponse> => {
const response = await apiClient.get<AdminSpinsResponse>('/cabinet/admin/wheel/spins', {
params,
})
return response.data
},
}