mirror of
https://github.com/chillpadclub/bedolaga-cabinet.git
synced 2026-07-30 02:23:47 +00:00
Add files via upload
This commit is contained in:
206
src/api/admin.ts
Normal file
206
src/api/admin.ts
Normal 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
119
src/api/adminApps.ts
Normal 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
73
src/api/adminSettings.ts
Normal 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
92
src/api/auth.ts
Normal 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
69
src/api/balance.ts
Normal 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
48
src/api/branding.ts
Normal 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
88
src/api/client.ts
Normal 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
57
src/api/contests.ts
Normal 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
98
src/api/currency.ts
Normal 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
12
src/api/index.ts
Normal 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
102
src/api/info.ts
Normal 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
36
src/api/miniapp.ts
Normal 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
58
src/api/notifications.ts
Normal 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
85
src/api/polls.ts
Normal 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
69
src/api/promo.ts
Normal 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
62
src/api/referral.ts
Normal 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
142
src/api/servers.ts
Normal 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
324
src/api/subscription.ts
Normal 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
228
src/api/tariffs.ts
Normal 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
43
src/api/themeColors.ts
Normal 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
84
src/api/tickets.ts
Normal 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
281
src/api/wheel.ts
Normal 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
|
||||
},
|
||||
}
|
||||
Reference in New Issue
Block a user