refactor: migrate to eslint flat config and format codebase with prettier

- Remove legacy .eslintrc.cjs and .eslintignore
- Add eslint.config.js with flat config, security rules (no-eval, no-implied-eval, no-new-func, no-script-url)
- Add .prettierrc and .prettierignore
- Format entire codebase with prettier
This commit is contained in:
c0mrade
2026-01-27 17:37:31 +03:00
parent 111ccc4e7a
commit bc90ba3779
133 changed files with 19972 additions and 15523 deletions

View File

@@ -1,306 +1,310 @@
import apiClient from './client'
import apiClient from './client';
export interface AdminTicketUser {
id: number
telegram_id: number
username: string | null
first_name: string | null
last_name: string | null
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
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
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[]
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
total: number;
open: number;
pending: number;
answered: number;
closed: number;
}
export interface TicketSettings {
sla_enabled: boolean
sla_minutes: number
sla_check_interval_seconds: number
sla_reminder_cooldown_minutes: number
support_system_mode: string // tickets, contact, both
cabinet_user_notifications_enabled: boolean
cabinet_admin_notifications_enabled: boolean
sla_enabled: boolean;
sla_minutes: number;
sla_check_interval_seconds: number;
sla_reminder_cooldown_minutes: number;
support_system_mode: string; // tickets, contact, both
cabinet_user_notifications_enabled: boolean;
cabinet_admin_notifications_enabled: boolean;
}
export interface TicketSettingsUpdate {
sla_enabled?: boolean
sla_minutes?: number
sla_check_interval_seconds?: number
sla_reminder_cooldown_minutes?: number
support_system_mode?: string
cabinet_user_notifications_enabled?: boolean
cabinet_admin_notifications_enabled?: boolean
sla_enabled?: boolean;
sla_minutes?: number;
sla_check_interval_seconds?: number;
sla_reminder_cooldown_minutes?: number;
support_system_mode?: string;
cabinet_user_notifications_enabled?: boolean;
cabinet_admin_notifications_enabled?: boolean;
}
export interface AdminTicketListResponse {
items: AdminTicket[]
total: number
page: number
per_page: number
pages: number
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
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
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
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
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
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
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
const response = await apiClient.post(`/cabinet/admin/tickets/${ticketId}/priority`, {
priority,
});
return response.data;
},
// Get ticket settings
getTicketSettings: async (): Promise<TicketSettings> => {
const response = await apiClient.get('/cabinet/admin/tickets/settings')
return response.data
const response = await apiClient.get('/cabinet/admin/tickets/settings');
return response.data;
},
// Update ticket settings
updateTicketSettings: async (settings: TicketSettingsUpdate): Promise<TicketSettings> => {
const response = await apiClient.patch('/cabinet/admin/tickets/settings', settings)
return response.data
const response = await apiClient.patch('/cabinet/admin/tickets/settings', settings);
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
xray_version?: string
node_version?: string
last_status_message?: string
xray_uptime?: string
is_xray_running?: boolean
cpu_count?: number
cpu_model?: string
total_ram?: string
country_code?: string
uuid: string;
name: string;
address: string;
is_connected: boolean;
is_disabled: boolean;
users_online: number;
traffic_used_bytes?: number;
uptime?: string;
xray_version?: string;
node_version?: string;
last_status_message?: string;
xray_uptime?: string;
is_xray_running?: boolean;
cpu_count?: number;
cpu_model?: string;
total_ram?: string;
country_code?: string;
}
export interface NodesOverview {
total: number
online: number
offline: number
disabled: number
total_users_online: number
nodes: NodeStatus[]
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
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
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
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
total_servers: number;
available_servers: number;
servers_with_connections: number;
total_revenue_kopeks: number;
total_revenue_rubles: number;
}
export interface TariffStatItem {
tariff_id: number
tariff_name: string
active_subscriptions: number
trial_subscriptions: number
purchased_today: number
purchased_week: number
purchased_month: number
tariff_id: number;
tariff_name: string;
active_subscriptions: number;
trial_subscriptions: number;
purchased_today: number;
purchased_week: number;
purchased_month: number;
}
export interface TariffStats {
tariffs: TariffStatItem[]
total_tariff_subscriptions: number
tariffs: TariffStatItem[];
total_tariff_subscriptions: number;
}
export interface DashboardStats {
nodes: NodesOverview
subscriptions: SubscriptionStats
financial: FinancialStats
servers: ServerStats
revenue_chart: RevenueData[]
tariff_stats?: TariffStats
nodes: NodesOverview;
subscriptions: SubscriptionStats;
financial: FinancialStats;
servers: ServerStats;
revenue_chart: RevenueData[];
tariff_stats?: TariffStats;
}
// ============ Extended Stats Types ============
export interface TopReferrerItem {
user_id: number
telegram_id: number
username?: string
display_name: string
invited_count: number
invited_today: number
invited_week: number
invited_month: number
earnings_today_kopeks: number
earnings_week_kopeks: number
earnings_month_kopeks: number
earnings_total_kopeks: number
user_id: number;
telegram_id: number;
username?: string;
display_name: string;
invited_count: number;
invited_today: number;
invited_week: number;
invited_month: number;
earnings_today_kopeks: number;
earnings_week_kopeks: number;
earnings_month_kopeks: number;
earnings_total_kopeks: number;
}
export interface TopReferrersResponse {
by_earnings: TopReferrerItem[]
by_invited: TopReferrerItem[]
total_referrers: number
total_referrals: number
total_earnings_kopeks: number
by_earnings: TopReferrerItem[];
by_invited: TopReferrerItem[];
total_referrers: number;
total_referrals: number;
total_earnings_kopeks: number;
}
export interface TopCampaignItem {
id: number
name: string
start_parameter: string
bonus_type: string
is_active: boolean
registrations: number
conversions: number
conversion_rate: number
total_revenue_kopeks: number
avg_revenue_per_user_kopeks: number
created_at?: string
id: number;
name: string;
start_parameter: string;
bonus_type: string;
is_active: boolean;
registrations: number;
conversions: number;
conversion_rate: number;
total_revenue_kopeks: number;
avg_revenue_per_user_kopeks: number;
created_at?: string;
}
export interface TopCampaignsResponse {
campaigns: TopCampaignItem[]
total_campaigns: number
total_registrations: number
total_revenue_kopeks: number
campaigns: TopCampaignItem[];
total_campaigns: number;
total_registrations: number;
total_revenue_kopeks: number;
}
export interface RecentPaymentItem {
id: number
user_id: number
telegram_id: number
username?: string
display_name: string
amount_kopeks: number
amount_rubles: number
type: string
type_display: string
payment_method?: string
description?: string
created_at: string
is_completed: boolean
id: number;
user_id: number;
telegram_id: number;
username?: string;
display_name: string;
amount_kopeks: number;
amount_rubles: number;
type: string;
type_display: string;
payment_method?: string;
description?: string;
created_at: string;
is_completed: boolean;
}
export interface RecentPaymentsResponse {
payments: RecentPaymentItem[]
total_count: number
total_today_kopeks: number
total_week_kopeks: number
payments: RecentPaymentItem[];
total_count: number;
total_today_kopeks: number;
total_week_kopeks: number;
}
// ============ Dashboard Stats API ============
@@ -308,43 +312,51 @@ export interface RecentPaymentsResponse {
export const statsApi = {
// Get complete dashboard stats
getDashboardStats: async (): Promise<DashboardStats> => {
const response = await apiClient.get('/cabinet/admin/stats/dashboard')
return response.data
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
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
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
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;
},
// Get top referrers
getTopReferrers: async (limit: number = 20): Promise<TopReferrersResponse> => {
const response = await apiClient.get('/cabinet/admin/stats/referrals/top', { params: { limit } })
return response.data
const response = await apiClient.get('/cabinet/admin/stats/referrals/top', {
params: { limit },
});
return response.data;
},
// Get top campaigns
getTopCampaigns: async (limit: number = 20): Promise<TopCampaignsResponse> => {
const response = await apiClient.get('/cabinet/admin/stats/campaigns/top', { params: { limit } })
return response.data
const response = await apiClient.get('/cabinet/admin/stats/campaigns/top', {
params: { limit },
});
return response.data;
},
// Get recent payments
getRecentPayments: async (limit: number = 50): Promise<RecentPaymentsResponse> => {
const response = await apiClient.get('/cabinet/admin/stats/payments/recent', { params: { limit } })
return response.data
const response = await apiClient.get('/cabinet/admin/stats/payments/recent', {
params: { limit },
});
return response.data;
},
}
};

View File

@@ -1,220 +1,240 @@
import apiClient from './client'
import apiClient from './client';
export interface LocalizedText {
en: string
ru: string
zh?: string
fa?: string
fr?: string
en: string;
ru: string;
zh?: string;
fa?: string;
fr?: string;
}
export interface AppButton {
id?: string // Unique identifier for React key (client-side only)
buttonLink: string
buttonText: LocalizedText
id?: string; // Unique identifier for React key (client-side only)
buttonLink: string;
buttonText: LocalizedText;
}
export interface AppStep {
description: LocalizedText
buttons?: AppButton[]
title?: LocalizedText
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
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
name: string;
logoUrl: string;
supportUrl: string;
}
export interface AppConfigConfig {
additionalLocales: string[]
branding: AppConfigBranding
additionalLocales: string[];
branding: AppConfigBranding;
}
export interface AppConfigResponse {
config: AppConfigConfig
platforms: Record<string, AppDefinition[]>
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
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
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
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
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
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}`)
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 }> => {
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
`/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
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
});
return response.data;
},
// Get RemnaWave config status
getRemnaWaveStatus: async (): Promise<{ enabled: boolean; config_uuid: string | null }> => {
const response = await apiClient.get<{ enabled: boolean; config_uuid: string | null }>(
'/cabinet/admin/apps/remnawave/status'
)
return response.data
'/cabinet/admin/apps/remnawave/status',
);
return response.data;
},
// Set RemnaWave config UUID
setRemnaWaveUuid: async (uuid: string | null): Promise<{ enabled: boolean; config_uuid: string | null }> => {
setRemnaWaveUuid: async (
uuid: string | null,
): Promise<{ enabled: boolean; config_uuid: string | null }> => {
const response = await apiClient.put<{ enabled: boolean; config_uuid: string | null }>(
'/cabinet/admin/apps/remnawave/uuid',
{ uuid }
)
return response.data
{ uuid },
);
return response.data;
},
// List available RemnaWave configs
listRemnaWaveConfigs: async (): Promise<{ uuid: string; name: string; view_position: number }[]> => {
listRemnaWaveConfigs: async (): Promise<
{ uuid: string; name: string; view_position: number }[]
> => {
const response = await apiClient.get<{ uuid: string; name: string; view_position: number }[]>(
'/cabinet/admin/apps/remnawave/configs'
)
return response.data
'/cabinet/admin/apps/remnawave/configs',
);
return response.data;
},
// Get RemnaWave subscription config
getRemnaWaveConfig: async (): Promise<RemnawaveConfig> => {
const response = await apiClient.get<{
uuid: string
name: string
view_position: number
config: RemnawaveConfig
}>('/cabinet/admin/apps/remnawave/config')
return response.data.config
uuid: string;
name: string;
view_position: number;
config: RemnawaveConfig;
}>('/cabinet/admin/apps/remnawave/config');
return response.data.config;
},
}
};
// ============== RemnaWave Format Types ==============
export interface RemnawaveButton {
url: string
text: LocalizedText
url: string;
text: LocalizedText;
}
export interface RemnawaveBlock {
title: LocalizedText
description: LocalizedText
buttons?: RemnawaveButton[]
svgIconKey?: string
svgIconColor?: string
title: LocalizedText;
description: LocalizedText;
buttons?: RemnawaveButton[];
svgIconKey?: string;
svgIconColor?: string;
}
export interface RemnawaveApp {
name: string
featured?: boolean
urlScheme?: string
isNeedBase64Encoding?: boolean
blocks: RemnawaveBlock[]
name: string;
featured?: boolean;
urlScheme?: string;
isNeedBase64Encoding?: boolean;
blocks: RemnawaveBlock[];
}
export interface RemnawavePlatform {
apps: RemnawaveApp[]
apps: RemnawaveApp[];
}
export interface RemnawaveSvgItem {
svgString: string
tags?: string[]
svgString: string;
tags?: string[];
}
export interface RemnawaveBaseSettings {
isShowTutorialButton: boolean
tutorialUrl: string
isShowTutorialButton: boolean;
tutorialUrl: string;
}
export interface RemnawaveBaseTranslations {
installApp: LocalizedText
addSubscription: LocalizedText
connectAndUse: LocalizedText
copyLink: LocalizedText
openApp: LocalizedText
tutorial: LocalizedText
close: LocalizedText
installApp: LocalizedText;
addSubscription: LocalizedText;
connectAndUse: LocalizedText;
copyLink: LocalizedText;
openApp: LocalizedText;
tutorial: LocalizedText;
close: LocalizedText;
}
export interface RemnawaveBrandingSettings {
name: string
logoUrl: string
supportUrl: string
name: string;
logoUrl: string;
supportUrl: string;
}
export interface RemnawaveConfig {
platforms: Record<string, RemnawavePlatform>
svgLibrary?: Record<string, RemnawaveSvgItem>
baseSettings?: RemnawaveBaseSettings
baseTranslations?: RemnawaveBaseTranslations
brandingSettings?: RemnawaveBrandingSettings
platforms: Record<string, RemnawavePlatform>;
svgLibrary?: Record<string, RemnawaveSvgItem>;
baseSettings?: RemnawaveBaseSettings;
baseTranslations?: RemnawaveBaseTranslations;
brandingSettings?: RemnawaveBrandingSettings;
}
// ============== Converter Functions ==============
@@ -225,23 +245,29 @@ const emptyLocalizedText = (): LocalizedText => ({
zh: '',
fa: '',
fr: '',
})
});
// Convert Cabinet button to RemnaWave button
const cabinetButtonToRemnawave = (button: AppButton): RemnawaveButton => ({
url: button.buttonLink,
text: { ...emptyLocalizedText(), ...button.buttonText },
})
});
// Convert RemnaWave button to Cabinet button
const remnawaveButtonToCabinet = (button: RemnawaveButton): AppButton => ({
buttonLink: button.url,
buttonText: { en: button.text.en || '', ru: button.text.ru || '', zh: button.text.zh, fa: button.text.fa, fr: button.text.fr },
})
buttonText: {
en: button.text.en || '',
ru: button.text.ru || '',
zh: button.text.zh,
fa: button.text.fa,
fr: button.text.fr,
},
});
// Convert Cabinet app to RemnaWave app format
export const cabinetAppToRemnawave = (app: AppDefinition): RemnawaveApp => {
const blocks: RemnawaveBlock[] = []
const blocks: RemnawaveBlock[] = [];
// Block 1: Installation
blocks.push({
@@ -250,17 +276,27 @@ export const cabinetAppToRemnawave = (app: AppDefinition): RemnawaveApp => {
buttons: app.installationStep.buttons?.map(cabinetButtonToRemnawave),
svgIconKey: 'download',
svgIconColor: '#3B82F6',
})
});
// Block 2 (optional): Additional before subscription
if (app.additionalBeforeAddSubscriptionStep?.description?.en || app.additionalBeforeAddSubscriptionStep?.description?.ru) {
if (
app.additionalBeforeAddSubscriptionStep?.description?.en ||
app.additionalBeforeAddSubscriptionStep?.description?.ru
) {
blocks.push({
title: app.additionalBeforeAddSubscriptionStep.title || { ...emptyLocalizedText(), en: 'Preparation', ru: 'Подготовка' },
description: { ...emptyLocalizedText(), ...app.additionalBeforeAddSubscriptionStep.description },
title: app.additionalBeforeAddSubscriptionStep.title || {
...emptyLocalizedText(),
en: 'Preparation',
ru: 'Подготовка',
},
description: {
...emptyLocalizedText(),
...app.additionalBeforeAddSubscriptionStep.description,
},
buttons: app.additionalBeforeAddSubscriptionStep.buttons?.map(cabinetButtonToRemnawave),
svgIconKey: 'settings',
svgIconColor: '#8B5CF6',
})
});
}
// Block 3: Add subscription
@@ -270,17 +306,27 @@ export const cabinetAppToRemnawave = (app: AppDefinition): RemnawaveApp => {
buttons: app.addSubscriptionStep.buttons?.map(cabinetButtonToRemnawave),
svgIconKey: 'plus',
svgIconColor: '#10B981',
})
});
// Block 4 (optional): Additional after subscription
if (app.additionalAfterAddSubscriptionStep?.description?.en || app.additionalAfterAddSubscriptionStep?.description?.ru) {
if (
app.additionalAfterAddSubscriptionStep?.description?.en ||
app.additionalAfterAddSubscriptionStep?.description?.ru
) {
blocks.push({
title: app.additionalAfterAddSubscriptionStep.title || { ...emptyLocalizedText(), en: 'Configuration', ru: 'Настройка' },
description: { ...emptyLocalizedText(), ...app.additionalAfterAddSubscriptionStep.description },
title: app.additionalAfterAddSubscriptionStep.title || {
...emptyLocalizedText(),
en: 'Configuration',
ru: 'Настройка',
},
description: {
...emptyLocalizedText(),
...app.additionalAfterAddSubscriptionStep.description,
},
buttons: app.additionalAfterAddSubscriptionStep.buttons?.map(cabinetButtonToRemnawave),
svgIconKey: 'settings',
svgIconColor: '#F59E0B',
})
});
}
// Block 5: Connect and use
@@ -290,7 +336,7 @@ export const cabinetAppToRemnawave = (app: AppDefinition): RemnawaveApp => {
buttons: app.connectAndUseStep.buttons?.map(cabinetButtonToRemnawave),
svgIconKey: 'check',
svgIconColor: '#22C55E',
})
});
return {
name: app.name,
@@ -298,78 +344,104 @@ export const cabinetAppToRemnawave = (app: AppDefinition): RemnawaveApp => {
urlScheme: app.urlScheme,
isNeedBase64Encoding: app.isNeedBase64Encoding,
blocks,
}
}
};
};
// Convert RemnaWave app to Cabinet app format
export const remnawaveAppToCabinet = (app: RemnawaveApp, platform: string): AppDefinition => {
const blocks = app.blocks || []
const blocks = app.blocks || [];
// Default empty step
const emptyStep = (): AppStep => ({
description: emptyLocalizedText(),
buttons: [],
})
});
// Try to identify blocks by their titles or position
let installationBlock: RemnawaveBlock | undefined
let subscriptionBlock: RemnawaveBlock | undefined
let connectBlock: RemnawaveBlock | undefined
let beforeSubBlock: RemnawaveBlock | undefined
let afterSubBlock: RemnawaveBlock | undefined
let installationBlock: RemnawaveBlock | undefined;
let subscriptionBlock: RemnawaveBlock | undefined;
let connectBlock: RemnawaveBlock | undefined;
let beforeSubBlock: RemnawaveBlock | undefined;
let afterSubBlock: RemnawaveBlock | undefined;
// First pass: try to identify by title keywords
for (const block of blocks) {
const enTitle = (block.title?.en || '').toLowerCase()
const ruTitle = (block.title?.ru || '').toLowerCase()
const enTitle = (block.title?.en || '').toLowerCase();
const ruTitle = (block.title?.ru || '').toLowerCase();
if (enTitle.includes('install') || ruTitle.includes('установ') || ruTitle.includes('скачай')) {
if (!installationBlock) installationBlock = block
} else if (enTitle.includes('subscription') || enTitle.includes('add') || ruTitle.includes('подписк') || ruTitle.includes('добав')) {
if (!subscriptionBlock) subscriptionBlock = block
} else if (enTitle.includes('connect') || enTitle.includes('use') || ruTitle.includes('подключ') || ruTitle.includes('использ')) {
if (!connectBlock) connectBlock = block
if (!installationBlock) installationBlock = block;
} else if (
enTitle.includes('subscription') ||
enTitle.includes('add') ||
ruTitle.includes('подписк') ||
ruTitle.includes('добав')
) {
if (!subscriptionBlock) subscriptionBlock = block;
} else if (
enTitle.includes('connect') ||
enTitle.includes('use') ||
ruTitle.includes('подключ') ||
ruTitle.includes('использ')
) {
if (!connectBlock) connectBlock = block;
}
}
// Second pass: assign remaining blocks by position if not found by title
if (!installationBlock && blocks.length > 0) {
installationBlock = blocks[0]
installationBlock = blocks[0];
}
if (!subscriptionBlock && blocks.length > 1) {
subscriptionBlock = blocks.find(b => b !== installationBlock) || blocks[1]
subscriptionBlock = blocks.find((b) => b !== installationBlock) || blocks[1];
}
if (!connectBlock && blocks.length > 2) {
connectBlock = blocks.find(b => b !== installationBlock && b !== subscriptionBlock) || blocks[blocks.length - 1]
connectBlock =
blocks.find((b) => b !== installationBlock && b !== subscriptionBlock) ||
blocks[blocks.length - 1];
}
// Assign additional blocks
const additionalBlocks = blocks.filter(b =>
b !== installationBlock && b !== subscriptionBlock && b !== connectBlock
)
const additionalBlocks = blocks.filter(
(b) => b !== installationBlock && b !== subscriptionBlock && b !== connectBlock,
);
if (additionalBlocks.length >= 1) {
// Check if block appears before subscription
const subIndex = blocks.indexOf(subscriptionBlock!)
const firstAdditionalIndex = blocks.indexOf(additionalBlocks[0])
const subIndex = blocks.indexOf(subscriptionBlock!);
const firstAdditionalIndex = blocks.indexOf(additionalBlocks[0]);
if (firstAdditionalIndex < subIndex) {
beforeSubBlock = additionalBlocks[0]
beforeSubBlock = additionalBlocks[0];
if (additionalBlocks.length >= 2) {
afterSubBlock = additionalBlocks[1]
afterSubBlock = additionalBlocks[1];
}
} else {
afterSubBlock = additionalBlocks[0]
afterSubBlock = additionalBlocks[0];
}
}
// Convert block to cabinet step
const blockToStep = (block: RemnawaveBlock | undefined): AppStep => {
if (!block) return emptyStep()
if (!block) return emptyStep();
return {
description: { en: block.description?.en || '', ru: block.description?.ru || '', zh: block.description?.zh, fa: block.description?.fa, fr: block.description?.fr },
title: block.title ? { en: block.title.en || '', ru: block.title.ru || '', zh: block.title.zh, fa: block.title.fa, fr: block.title.fr } : undefined,
description: {
en: block.description?.en || '',
ru: block.description?.ru || '',
zh: block.description?.zh,
fa: block.description?.fa,
fr: block.description?.fr,
},
title: block.title
? {
en: block.title.en || '',
ru: block.title.ru || '',
zh: block.title.zh,
fa: block.title.fa,
fr: block.title.fr,
}
: undefined,
buttons: block.buttons?.map(remnawaveButtonToCabinet),
}
}
};
};
return {
id: `${app.name.toLowerCase().replace(/[^a-z0-9]/g, '-')}-${platform}-${Date.now()}`,
@@ -382,66 +454,114 @@ export const remnawaveAppToCabinet = (app: RemnawaveApp, platform: string): AppD
connectAndUseStep: blockToStep(connectBlock),
additionalBeforeAddSubscriptionStep: beforeSubBlock ? blockToStep(beforeSubBlock) : undefined,
additionalAfterAddSubscriptionStep: afterSubBlock ? blockToStep(afterSubBlock) : undefined,
}
}
};
};
// Export all apps from cabinet to RemnaWave format
export const exportToRemnawaveFormat = (
platformApps: Record<string, AppDefinition[]>,
branding?: AppConfigBranding
branding?: AppConfigBranding,
): RemnawaveConfig => {
const platforms: Record<string, RemnawavePlatform> = {}
const platforms: Record<string, RemnawavePlatform> = {};
for (const [platform, apps] of Object.entries(platformApps)) {
platforms[platform] = {
apps: apps.map(cabinetAppToRemnawave),
}
};
}
return {
platforms,
svgLibrary: {
download: { svgString: '<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5M16.5 12L12 16.5m0 0L7.5 12m4.5 4.5V3" /></svg>' },
plus: { svgString: '<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15" /></svg>' },
check: { svgString: '<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5" /></svg>' },
settings: { svgString: '<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M9.594 3.94c.09-.542.56-.94 1.11-.94h2.593c.55 0 1.02.398 1.11.94l.213 1.281c.063.374.313.686.645.87.074.04.147.083.22.127.324.196.72.257 1.075.124l1.217-.456a1.125 1.125 0 011.37.49l1.296 2.247a1.125 1.125 0 01-.26 1.431l-1.003.827c-.293.24-.438.613-.431.992a6.759 6.759 0 010 .255c-.007.378.138.75.43.99l1.005.828c.424.35.534.954.26 1.43l-1.298 2.247a1.125 1.125 0 01-1.369.491l-1.217-.456c-.355-.133-.75-.072-1.076.124a6.57 6.57 0 01-.22.128c-.331.183-.581.495-.644.869l-.213 1.28c-.09.543-.56.941-1.11.941h-2.594c-.55 0-1.02-.398-1.11-.94l-.213-1.281c-.062-.374-.312-.686-.644-.87a6.52 6.52 0 01-.22-.127c-.325-.196-.72-.257-1.076-.124l-1.217.456a1.125 1.125 0 01-1.369-.49l-1.297-2.247a1.125 1.125 0 01.26-1.431l1.004-.827c.292-.24.437-.613.43-.992a6.932 6.932 0 010-.255c.007-.378-.138-.75-.43-.99l-1.004-.828a1.125 1.125 0 01-.26-1.43l1.297-2.247a1.125 1.125 0 011.37-.491l1.216.456c.356.133.751.072 1.076-.124.072-.044.146-.087.22-.128.332-.183.582-.495.644-.869l.214-1.281z" /><path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" /></svg>' },
download: {
svgString:
'<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5M16.5 12L12 16.5m0 0L7.5 12m4.5 4.5V3" /></svg>',
},
plus: {
svgString:
'<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15" /></svg>',
},
check: {
svgString:
'<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5" /></svg>',
},
settings: {
svgString:
'<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M9.594 3.94c.09-.542.56-.94 1.11-.94h2.593c.55 0 1.02.398 1.11.94l.213 1.281c.063.374.313.686.645.87.074.04.147.083.22.127.324.196.72.257 1.075.124l1.217-.456a1.125 1.125 0 011.37.49l1.296 2.247a1.125 1.125 0 01-.26 1.431l-1.003.827c-.293.24-.438.613-.431.992a6.759 6.759 0 010 .255c-.007.378.138.75.43.99l1.005.828c.424.35.534.954.26 1.43l-1.298 2.247a1.125 1.125 0 01-1.369.491l-1.217-.456c-.355-.133-.75-.072-1.076.124a6.57 6.57 0 01-.22.128c-.331.183-.581.495-.644.869l-.213 1.28c-.09.543-.56.941-1.11.941h-2.594c-.55 0-1.02-.398-1.11-.94l-.213-1.281c-.062-.374-.312-.686-.644-.87a6.52 6.52 0 01-.22-.127c-.325-.196-.72-.257-1.076-.124l-1.217.456a1.125 1.125 0 01-1.369-.49l-1.297-2.247a1.125 1.125 0 01.26-1.431l1.004-.827c.292-.24.437-.613.43-.992a6.932 6.932 0 010-.255c.007-.378-.138-.75-.43-.99l-1.004-.828a1.125 1.125 0 01-.26-1.43l1.297-2.247a1.125 1.125 0 011.37-.491l1.216.456c.356.133.751.072 1.076-.124.072-.044.146-.087.22-.128.332-.183.582-.495.644-.869l.214-1.281z" /><path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" /></svg>',
},
},
baseSettings: {
isShowTutorialButton: false,
tutorialUrl: '',
},
baseTranslations: {
installApp: { en: 'Install App', ru: 'Установить приложение', zh: '安装应用', fa: 'نصب برنامه', fr: 'Installer l\'application' },
addSubscription: { en: 'Add Subscription', ru: 'Добавить подписку', zh: '添加订阅', fa: 'اضافه کردن اشتراک', fr: 'Ajouter un abonnement' },
connectAndUse: { en: 'Connect and Use', ru: 'Подключиться и использовать', zh: '连接并使用', fa: 'اتصال و استفاده', fr: 'Connecter et utiliser' },
copyLink: { en: 'Copy Link', ru: 'Скопировать ссылку', zh: '复制链接', fa: 'کپی لینک', fr: 'Copier le lien' },
openApp: { en: 'Open App', ru: 'Открыть приложение', zh: '打开应用', fa: 'باز کردن برنامه', fr: 'Ouvrir l\'application' },
installApp: {
en: 'Install App',
ru: 'Установить приложение',
zh: '安装应用',
fa: صب برنامه',
fr: "Installer l'application",
},
addSubscription: {
en: 'Add Subscription',
ru: 'Добавить подписку',
zh: '添加订阅',
fa: 'اضافه کردن اشتراک',
fr: 'Ajouter un abonnement',
},
connectAndUse: {
en: 'Connect and Use',
ru: 'Подключиться и использовать',
zh: '连接并使用',
fa: 'اتصال و استفاده',
fr: 'Connecter et utiliser',
},
copyLink: {
en: 'Copy Link',
ru: 'Скопировать ссылку',
zh: '复制链接',
fa: 'کپی لینک',
fr: 'Copier le lien',
},
openApp: {
en: 'Open App',
ru: 'Открыть приложение',
zh: '打开应用',
fa: 'باز کردن برنامه',
fr: "Ouvrir l'application",
},
tutorial: { en: 'Tutorial', ru: 'Инструкция', zh: '教程', fa: 'آموزش', fr: 'Tutoriel' },
close: { en: 'Close', ru: 'Закрыть', zh: '关闭', fa: 'بستن', fr: 'Fermer' },
},
brandingSettings: branding ? {
name: branding.name,
logoUrl: branding.logoUrl,
supportUrl: branding.supportUrl,
} : undefined,
}
}
brandingSettings: branding
? {
name: branding.name,
logoUrl: branding.logoUrl,
supportUrl: branding.supportUrl,
}
: undefined,
};
};
// Import apps from RemnaWave format to cabinet
export const importFromRemnawaveFormat = (
config: RemnawaveConfig
config: RemnawaveConfig,
): { platformApps: Record<string, AppDefinition[]>; branding?: AppConfigBranding } => {
const platformApps: Record<string, AppDefinition[]> = {}
const platformApps: Record<string, AppDefinition[]> = {};
for (const [platform, platformData] of Object.entries(config.platforms || {})) {
platformApps[platform] = (platformData.apps || []).map(app => remnawaveAppToCabinet(app, platform))
platformApps[platform] = (platformData.apps || []).map((app) =>
remnawaveAppToCabinet(app, platform),
);
}
const branding = config.brandingSettings ? {
name: config.brandingSettings.name,
logoUrl: config.brandingSettings.logoUrl,
supportUrl: config.brandingSettings.supportUrl,
} : undefined
const branding = config.brandingSettings
? {
name: config.brandingSettings.name,
logoUrl: config.brandingSettings.logoUrl,
supportUrl: config.brandingSettings.supportUrl,
}
: undefined;
return { platformApps, branding }
}
return { platformApps, branding };
};

View File

@@ -1,168 +1,187 @@
import apiClient from './client'
import apiClient from './client';
// Types
export interface BroadcastFilter {
key: string
label: string
count: number | null
group: string | null
key: string;
label: string;
count: number | null;
group: string | null;
}
export interface TariffFilter {
key: string
label: string
tariff_id: number
count: number
key: string;
label: string;
tariff_id: number;
count: number;
}
export interface BroadcastFiltersResponse {
filters: BroadcastFilter[]
tariff_filters: TariffFilter[]
custom_filters: BroadcastFilter[]
filters: BroadcastFilter[];
tariff_filters: TariffFilter[];
custom_filters: BroadcastFilter[];
}
export interface TariffForBroadcast {
id: number
name: string
filter_key: string
active_users_count: number
id: number;
name: string;
filter_key: string;
active_users_count: number;
}
export interface BroadcastTariffsResponse {
tariffs: TariffForBroadcast[]
tariffs: TariffForBroadcast[];
}
export interface BroadcastButton {
key: string
label: string
default: boolean
key: string;
label: string;
default: boolean;
}
export interface BroadcastButtonsResponse {
buttons: BroadcastButton[]
buttons: BroadcastButton[];
}
export interface BroadcastMedia {
type: 'photo' | 'video' | 'document'
file_id: string
caption?: string
type: 'photo' | 'video' | 'document';
file_id: string;
caption?: string;
}
export interface BroadcastCreateRequest {
target: string
message_text: string
selected_buttons: string[]
media?: BroadcastMedia
target: string;
message_text: string;
selected_buttons: string[];
media?: BroadcastMedia;
}
export interface Broadcast {
id: number
target_type: string
message_text: string
has_media: boolean
media_type: string | null
media_file_id: string | null
media_caption: string | null
total_count: number
sent_count: number
failed_count: number
status: 'queued' | 'in_progress' | 'completed' | 'partial' | 'failed' | 'cancelled' | 'cancelling'
admin_id: number | null
admin_name: string | null
created_at: string
completed_at: string | null
progress_percent: number
id: number;
target_type: string;
message_text: string;
has_media: boolean;
media_type: string | null;
media_file_id: string | null;
media_caption: string | null;
total_count: number;
sent_count: number;
failed_count: number;
status:
| 'queued'
| 'in_progress'
| 'completed'
| 'partial'
| 'failed'
| 'cancelled'
| 'cancelling';
admin_id: number | null;
admin_name: string | null;
created_at: string;
completed_at: string | null;
progress_percent: number;
}
export interface BroadcastListResponse {
items: Broadcast[]
total: number
limit: number
offset: number
items: Broadcast[];
total: number;
limit: number;
offset: number;
}
export interface BroadcastPreviewRequest {
target: string
target: string;
}
export interface BroadcastPreviewResponse {
target: string
count: number
target: string;
count: number;
}
export interface MediaUploadResponse {
media_type: string
file_id: string
file_unique_id: string | null
media_url: string
media_type: string;
file_id: string;
file_unique_id: string | null;
media_url: string;
}
export const adminBroadcastsApi = {
// Get all available filters with counts
getFilters: async (): Promise<BroadcastFiltersResponse> => {
const response = await apiClient.get<BroadcastFiltersResponse>('/cabinet/admin/broadcasts/filters')
return response.data
const response = await apiClient.get<BroadcastFiltersResponse>(
'/cabinet/admin/broadcasts/filters',
);
return response.data;
},
// Get tariffs for filtering
getTariffs: async (): Promise<BroadcastTariffsResponse> => {
const response = await apiClient.get<BroadcastTariffsResponse>('/cabinet/admin/broadcasts/tariffs')
return response.data
const response = await apiClient.get<BroadcastTariffsResponse>(
'/cabinet/admin/broadcasts/tariffs',
);
return response.data;
},
// Get available buttons
getButtons: async (): Promise<BroadcastButtonsResponse> => {
const response = await apiClient.get<BroadcastButtonsResponse>('/cabinet/admin/broadcasts/buttons')
return response.data
const response = await apiClient.get<BroadcastButtonsResponse>(
'/cabinet/admin/broadcasts/buttons',
);
return response.data;
},
// Preview broadcast (get recipients count)
preview: async (target: string): Promise<BroadcastPreviewResponse> => {
const response = await apiClient.post<BroadcastPreviewResponse>('/cabinet/admin/broadcasts/preview', {
target,
})
return response.data
const response = await apiClient.post<BroadcastPreviewResponse>(
'/cabinet/admin/broadcasts/preview',
{
target,
},
);
return response.data;
},
// Create and start broadcast
create: async (data: BroadcastCreateRequest): Promise<Broadcast> => {
const response = await apiClient.post<Broadcast>('/cabinet/admin/broadcasts', data)
return response.data
const response = await apiClient.post<Broadcast>('/cabinet/admin/broadcasts', data);
return response.data;
},
// Get list of broadcasts
list: async (limit = 20, offset = 0): Promise<BroadcastListResponse> => {
const response = await apiClient.get<BroadcastListResponse>('/cabinet/admin/broadcasts', {
params: { limit, offset },
})
return response.data
});
return response.data;
},
// Get broadcast details
get: async (id: number): Promise<Broadcast> => {
const response = await apiClient.get<Broadcast>(`/cabinet/admin/broadcasts/${id}`)
return response.data
const response = await apiClient.get<Broadcast>(`/cabinet/admin/broadcasts/${id}`);
return response.data;
},
// Stop broadcast
stop: async (id: number): Promise<Broadcast> => {
const response = await apiClient.post<Broadcast>(`/cabinet/admin/broadcasts/${id}/stop`)
return response.data
const response = await apiClient.post<Broadcast>(`/cabinet/admin/broadcasts/${id}/stop`);
return response.data;
},
// Upload media (uses existing media endpoint)
uploadMedia: async (file: File, mediaType: 'photo' | 'video' | 'document'): Promise<MediaUploadResponse> => {
const formData = new FormData()
formData.append('file', file)
formData.append('media_type', mediaType)
uploadMedia: async (
file: File,
mediaType: 'photo' | 'video' | 'document',
): 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
});
return response.data;
},
}
};
export default adminBroadcastsApi
export default adminBroadcastsApi;

View File

@@ -1,85 +1,105 @@
import apiClient from './client'
import apiClient from './client';
export interface EmailTemplateLanguageStatus {
has_custom: boolean
has_custom: boolean;
}
export interface EmailTemplateType {
type: string
label: Record<string, string>
description: Record<string, string>
context_vars: string[]
languages: Record<string, EmailTemplateLanguageStatus>
type: string;
label: Record<string, string>;
description: Record<string, string>;
context_vars: string[];
languages: Record<string, EmailTemplateLanguageStatus>;
}
export interface EmailTemplateListResponse {
items: EmailTemplateType[]
available_languages: string[]
items: EmailTemplateType[];
available_languages: string[];
}
export interface EmailTemplateLanguageData {
subject: string
body_html: string
is_default: boolean
default_subject: string
default_body_html: string
subject: string;
body_html: string;
is_default: boolean;
default_subject: string;
default_body_html: string;
}
export interface EmailTemplateDetail {
notification_type: string
label: Record<string, string>
description: Record<string, string>
context_vars: string[]
languages: Record<string, EmailTemplateLanguageData>
notification_type: string;
label: Record<string, string>;
description: Record<string, string>;
context_vars: string[];
languages: Record<string, EmailTemplateLanguageData>;
}
export interface EmailTemplateUpdateRequest {
subject: string
body_html: string
subject: string;
body_html: string;
}
export interface EmailTemplatePreviewRequest {
language: string
subject?: string
body_html?: string
language: string;
subject?: string;
body_html?: string;
}
export interface EmailTemplatePreviewResponse {
subject: string
body_html: string
subject: string;
body_html: string;
}
export interface EmailTemplateSendTestRequest {
language: string
email?: string
language: string;
email?: string;
}
export const adminEmailTemplatesApi = {
getTemplateTypes: async (): Promise<EmailTemplateListResponse> => {
const response = await apiClient.get<EmailTemplateListResponse>('/cabinet/admin/email-templates')
return response.data
const response = await apiClient.get<EmailTemplateListResponse>(
'/cabinet/admin/email-templates',
);
return response.data;
},
getTemplate: async (notificationType: string): Promise<EmailTemplateDetail> => {
const response = await apiClient.get<EmailTemplateDetail>(`/cabinet/admin/email-templates/${notificationType}`)
return response.data
const response = await apiClient.get<EmailTemplateDetail>(
`/cabinet/admin/email-templates/${notificationType}`,
);
return response.data;
},
updateTemplate: async (notificationType: string, language: string, data: EmailTemplateUpdateRequest): Promise<void> => {
await apiClient.put(`/cabinet/admin/email-templates/${notificationType}/${language}`, data)
updateTemplate: async (
notificationType: string,
language: string,
data: EmailTemplateUpdateRequest,
): Promise<void> => {
await apiClient.put(`/cabinet/admin/email-templates/${notificationType}/${language}`, data);
},
deleteTemplate: async (notificationType: string, language: string): Promise<void> => {
await apiClient.delete(`/cabinet/admin/email-templates/${notificationType}/${language}`)
await apiClient.delete(`/cabinet/admin/email-templates/${notificationType}/${language}`);
},
previewTemplate: async (notificationType: string, data: EmailTemplatePreviewRequest): Promise<EmailTemplatePreviewResponse> => {
const response = await apiClient.post<EmailTemplatePreviewResponse>(`/cabinet/admin/email-templates/${notificationType}/preview`, data)
return response.data
previewTemplate: async (
notificationType: string,
data: EmailTemplatePreviewRequest,
): Promise<EmailTemplatePreviewResponse> => {
const response = await apiClient.post<EmailTemplatePreviewResponse>(
`/cabinet/admin/email-templates/${notificationType}/preview`,
data,
);
return response.data;
},
sendTestEmail: async (notificationType: string, data: EmailTemplateSendTestRequest): Promise<{ sent_to: string }> => {
const response = await apiClient.post<{ status: string; sent_to: string }>(`/cabinet/admin/email-templates/${notificationType}/test`, data)
return response.data
sendTestEmail: async (
notificationType: string,
data: EmailTemplateSendTestRequest,
): Promise<{ sent_to: string }> => {
const response = await apiClient.post<{ status: string; sent_to: string }>(
`/cabinet/admin/email-templates/${notificationType}/test`,
data,
);
return response.data;
},
}
};

View File

@@ -1,31 +1,35 @@
import apiClient from './client'
import type { PaymentMethodConfig, PromoGroupSimple } from '../types'
import apiClient from './client';
import type { PaymentMethodConfig, PromoGroupSimple } from '../types';
export const adminPaymentMethodsApi = {
getAll: async (): Promise<PaymentMethodConfig[]> => {
const response = await apiClient.get<PaymentMethodConfig[]>('/cabinet/admin/payment-methods')
return response.data
const response = await apiClient.get<PaymentMethodConfig[]>('/cabinet/admin/payment-methods');
return response.data;
},
getOne: async (methodId: string): Promise<PaymentMethodConfig> => {
const response = await apiClient.get<PaymentMethodConfig>(`/cabinet/admin/payment-methods/${methodId}`)
return response.data
const response = await apiClient.get<PaymentMethodConfig>(
`/cabinet/admin/payment-methods/${methodId}`,
);
return response.data;
},
update: async (methodId: string, data: Record<string, unknown>): Promise<PaymentMethodConfig> => {
const response = await apiClient.put<PaymentMethodConfig>(
`/cabinet/admin/payment-methods/${methodId}`,
data
)
return response.data
data,
);
return response.data;
},
updateOrder: async (methodIds: string[]): Promise<void> => {
await apiClient.put('/cabinet/admin/payment-methods/order', { method_ids: methodIds })
await apiClient.put('/cabinet/admin/payment-methods/order', { method_ids: methodIds });
},
getPromoGroups: async (): Promise<PromoGroupSimple[]> => {
const response = await apiClient.get<PromoGroupSimple[]>('/cabinet/admin/payment-methods/promo-groups')
return response.data
const response = await apiClient.get<PromoGroupSimple[]>(
'/cabinet/admin/payment-methods/promo-groups',
);
return response.data;
},
}
};

View File

@@ -1,39 +1,46 @@
import apiClient from './client'
import type { PaginatedResponse, PendingPayment, ManualCheckResponse } from '../types'
import apiClient from './client';
import type { PaginatedResponse, PendingPayment, ManualCheckResponse } from '../types';
export interface PaymentsStats {
total_pending: number
by_method: Record<string, number>
total_pending: number;
by_method: Record<string, number>;
}
export const adminPaymentsApi = {
// Get all pending payments (admin)
getPendingPayments: async (params?: {
page?: number
per_page?: number
method_filter?: string
page?: number;
per_page?: number;
method_filter?: string;
}): Promise<PaginatedResponse<PendingPayment>> => {
const response = await apiClient.get<PaginatedResponse<PendingPayment>>('/cabinet/admin/payments', {
params,
})
return response.data
const response = await apiClient.get<PaginatedResponse<PendingPayment>>(
'/cabinet/admin/payments',
{
params,
},
);
return response.data;
},
// Get payments statistics
getStats: async (): Promise<PaymentsStats> => {
const response = await apiClient.get<PaymentsStats>('/cabinet/admin/payments/stats')
return response.data
const response = await apiClient.get<PaymentsStats>('/cabinet/admin/payments/stats');
return response.data;
},
// Get specific payment details
getPayment: async (method: string, paymentId: number): Promise<PendingPayment> => {
const response = await apiClient.get<PendingPayment>(`/cabinet/admin/payments/${method}/${paymentId}`)
return response.data
const response = await apiClient.get<PendingPayment>(
`/cabinet/admin/payments/${method}/${paymentId}`,
);
return response.data;
},
// Manually check payment status
checkPaymentStatus: async (method: string, paymentId: number): Promise<ManualCheckResponse> => {
const response = await apiClient.post<ManualCheckResponse>(`/cabinet/admin/payments/${method}/${paymentId}/check`)
return response.data
const response = await apiClient.post<ManualCheckResponse>(
`/cabinet/admin/payments/${method}/${paymentId}/check`,
);
return response.data;
},
}
};

View File

@@ -1,227 +1,227 @@
import { apiClient } from './client'
import { apiClient } from './client';
// ============ Types ============
// Status & Connection
export interface ConnectionStatus {
status: string
message: string
api_url?: string
status_code?: number
system_info?: Record<string, unknown>
status: string;
message: string;
api_url?: string;
status_code?: number;
system_info?: Record<string, unknown>;
}
export interface RemnaWaveStatusResponse {
is_configured: boolean
configuration_error?: string
connection?: ConnectionStatus
is_configured: boolean;
configuration_error?: string;
connection?: ConnectionStatus;
}
// System Statistics
export interface SystemSummary {
users_online: number
total_users: number
active_connections: number
nodes_online: number
users_last_day: number
users_last_week: number
users_never_online: number
total_user_traffic: number
users_online: number;
total_users: number;
active_connections: number;
nodes_online: number;
users_last_day: number;
users_last_week: number;
users_never_online: number;
total_user_traffic: number;
}
export interface ServerInfo {
cpu_cores: number
cpu_physical_cores: number
memory_total: number
memory_used: number
memory_free: number
memory_available: number
uptime_seconds: number
cpu_cores: number;
cpu_physical_cores: number;
memory_total: number;
memory_used: number;
memory_free: number;
memory_available: number;
uptime_seconds: number;
}
export interface Bandwidth {
realtime_download: number
realtime_upload: number
realtime_total: number
realtime_download: number;
realtime_upload: number;
realtime_total: number;
}
export interface TrafficPeriod {
current: number
previous: number
difference?: string
current: number;
previous: number;
difference?: string;
}
export interface TrafficPeriods {
last_2_days: TrafficPeriod
last_7_days: TrafficPeriod
last_30_days: TrafficPeriod
current_month: TrafficPeriod
current_year: TrafficPeriod
last_2_days: TrafficPeriod;
last_7_days: TrafficPeriod;
last_30_days: TrafficPeriod;
current_month: TrafficPeriod;
current_year: TrafficPeriod;
}
export interface SystemStatsResponse {
system: SystemSummary
users_by_status: Record<string, number>
server_info: ServerInfo
bandwidth: Bandwidth
traffic_periods: TrafficPeriods
nodes_realtime: Record<string, unknown>[]
nodes_weekly: Record<string, unknown>[]
last_updated?: string
system: SystemSummary;
users_by_status: Record<string, number>;
server_info: ServerInfo;
bandwidth: Bandwidth;
traffic_periods: TrafficPeriods;
nodes_realtime: Record<string, unknown>[];
nodes_weekly: Record<string, unknown>[];
last_updated?: string;
}
// Nodes
export interface NodeInfo {
uuid: string
name: string
address: string
country_code?: string
is_connected: boolean
is_disabled: boolean
is_node_online: boolean
is_xray_running: boolean
users_online?: number
traffic_used_bytes?: number
traffic_limit_bytes?: number
last_status_change?: string
last_status_message?: string
xray_uptime?: string
is_traffic_tracking_active: boolean
traffic_reset_day?: number
notify_percent?: number
consumption_multiplier: number
cpu_count?: number
cpu_model?: string
total_ram?: string
created_at?: string
updated_at?: string
provider_uuid?: string
uuid: string;
name: string;
address: string;
country_code?: string;
is_connected: boolean;
is_disabled: boolean;
is_node_online: boolean;
is_xray_running: boolean;
users_online?: number;
traffic_used_bytes?: number;
traffic_limit_bytes?: number;
last_status_change?: string;
last_status_message?: string;
xray_uptime?: string;
is_traffic_tracking_active: boolean;
traffic_reset_day?: number;
notify_percent?: number;
consumption_multiplier: number;
cpu_count?: number;
cpu_model?: string;
total_ram?: string;
created_at?: string;
updated_at?: string;
provider_uuid?: string;
}
export interface NodesListResponse {
items: NodeInfo[]
total: number
items: NodeInfo[];
total: number;
}
export interface NodesOverview {
total: number
online: number
offline: number
disabled: number
total_users_online: number
nodes: NodeInfo[]
total: number;
online: number;
offline: number;
disabled: number;
total_users_online: number;
nodes: NodeInfo[];
}
export interface NodeStatisticsResponse {
node: NodeInfo
realtime?: Record<string, unknown>
usage_history: Record<string, unknown>[]
last_updated?: string
node: NodeInfo;
realtime?: Record<string, unknown>;
usage_history: Record<string, unknown>[];
last_updated?: string;
}
export interface NodeActionResponse {
success: boolean
message?: string
is_disabled?: boolean
success: boolean;
message?: string;
is_disabled?: boolean;
}
// Squads
export interface SquadWithLocalInfo {
uuid: string
name: string
members_count: number
inbounds_count: number
inbounds: Record<string, unknown>[]
local_id?: number
display_name?: string
country_code?: string
is_available?: boolean
is_trial_eligible?: boolean
price_kopeks?: number
max_users?: number
current_users?: number
is_synced: boolean
uuid: string;
name: string;
members_count: number;
inbounds_count: number;
inbounds: Record<string, unknown>[];
local_id?: number;
display_name?: string;
country_code?: string;
is_available?: boolean;
is_trial_eligible?: boolean;
price_kopeks?: number;
max_users?: number;
current_users?: number;
is_synced: boolean;
}
export interface SquadsListResponse {
items: SquadWithLocalInfo[]
total: number
items: SquadWithLocalInfo[];
total: number;
}
export interface SquadDetailResponse extends SquadWithLocalInfo {
description?: string
sort_order?: number
active_subscriptions: number
description?: string;
sort_order?: number;
active_subscriptions: number;
}
export interface SquadOperationResponse {
success: boolean
message?: string
data?: Record<string, unknown>
success: boolean;
message?: string;
data?: Record<string, unknown>;
}
// Migration
export interface MigrationPreviewResponse {
squad_uuid: string
squad_name: string
current_users: number
max_users?: number
users_to_migrate: number
squad_uuid: string;
squad_name: string;
current_users: number;
max_users?: number;
users_to_migrate: number;
}
export interface MigrationStats {
source_uuid: string
target_uuid: string
total: number
updated: number
panel_updated: number
panel_failed: number
source_removed: number
target_added: number
source_uuid: string;
target_uuid: string;
total: number;
updated: number;
panel_updated: number;
panel_failed: number;
source_removed: number;
target_added: number;
}
export interface MigrationResponse {
success: boolean
message?: string
error?: string
data?: MigrationStats
success: boolean;
message?: string;
error?: string;
data?: MigrationStats;
}
// Inbounds
export interface InboundsListResponse {
items: Record<string, unknown>[]
total: number
items: Record<string, unknown>[];
total: number;
}
// Auto Sync
export interface AutoSyncStatus {
enabled: boolean
times: string[]
next_run?: string
is_running: boolean
last_run_started_at?: string
last_run_finished_at?: string
last_run_success?: boolean
last_run_reason?: string
last_run_error?: string
last_user_stats?: Record<string, unknown>
last_server_stats?: Record<string, unknown>
enabled: boolean;
times: string[];
next_run?: string;
is_running: boolean;
last_run_started_at?: string;
last_run_finished_at?: string;
last_run_success?: boolean;
last_run_reason?: string;
last_run_error?: string;
last_user_stats?: Record<string, unknown>;
last_server_stats?: Record<string, unknown>;
}
export interface AutoSyncRunResponse {
started: boolean
success?: boolean
error?: string
user_stats?: Record<string, unknown>
server_stats?: Record<string, unknown>
reason?: string
started: boolean;
success?: boolean;
error?: string;
user_stats?: Record<string, unknown>;
server_stats?: Record<string, unknown>;
reason?: string;
}
// Sync
export interface SyncResponse {
success: boolean
message?: string
data?: Record<string, unknown>
success: boolean;
message?: string;
data?: Record<string, unknown>;
}
// ============ API ============
@@ -229,158 +229,176 @@ export interface SyncResponse {
export const adminRemnawaveApi = {
// Status & Connection
getStatus: async (): Promise<RemnaWaveStatusResponse> => {
const response = await apiClient.get('/cabinet/admin/remnawave/status')
return response.data
const response = await apiClient.get('/cabinet/admin/remnawave/status');
return response.data;
},
// System Statistics
getSystemStats: async (): Promise<SystemStatsResponse> => {
const response = await apiClient.get('/cabinet/admin/remnawave/system')
return response.data
const response = await apiClient.get('/cabinet/admin/remnawave/system');
return response.data;
},
// Nodes
getNodes: async (): Promise<NodesListResponse> => {
const response = await apiClient.get('/cabinet/admin/remnawave/nodes')
return response.data
const response = await apiClient.get('/cabinet/admin/remnawave/nodes');
return response.data;
},
getNodesOverview: async (): Promise<NodesOverview> => {
const response = await apiClient.get('/cabinet/admin/remnawave/nodes/overview')
return response.data
const response = await apiClient.get('/cabinet/admin/remnawave/nodes/overview');
return response.data;
},
getNodesRealtime: async (): Promise<Record<string, unknown>[]> => {
const response = await apiClient.get('/cabinet/admin/remnawave/nodes/realtime')
return response.data
const response = await apiClient.get('/cabinet/admin/remnawave/nodes/realtime');
return response.data;
},
getNode: async (uuid: string): Promise<NodeInfo> => {
const response = await apiClient.get(`/cabinet/admin/remnawave/nodes/${uuid}`)
return response.data
const response = await apiClient.get(`/cabinet/admin/remnawave/nodes/${uuid}`);
return response.data;
},
getNodeStatistics: async (uuid: string): Promise<NodeStatisticsResponse> => {
const response = await apiClient.get(`/cabinet/admin/remnawave/nodes/${uuid}/statistics`)
return response.data
const response = await apiClient.get(`/cabinet/admin/remnawave/nodes/${uuid}/statistics`);
return response.data;
},
nodeAction: async (uuid: string, action: 'enable' | 'disable' | 'restart'): Promise<NodeActionResponse> => {
const response = await apiClient.post(`/cabinet/admin/remnawave/nodes/${uuid}/action`, { action })
return response.data
nodeAction: async (
uuid: string,
action: 'enable' | 'disable' | 'restart',
): Promise<NodeActionResponse> => {
const response = await apiClient.post(`/cabinet/admin/remnawave/nodes/${uuid}/action`, {
action,
});
return response.data;
},
restartAllNodes: async (): Promise<NodeActionResponse> => {
const response = await apiClient.post('/cabinet/admin/remnawave/nodes/restart-all')
return response.data
const response = await apiClient.post('/cabinet/admin/remnawave/nodes/restart-all');
return response.data;
},
// Squads
getSquads: async (): Promise<SquadsListResponse> => {
const response = await apiClient.get('/cabinet/admin/remnawave/squads')
return response.data
const response = await apiClient.get('/cabinet/admin/remnawave/squads');
return response.data;
},
getSquad: async (uuid: string): Promise<SquadDetailResponse> => {
const response = await apiClient.get(`/cabinet/admin/remnawave/squads/${uuid}`)
return response.data
const response = await apiClient.get(`/cabinet/admin/remnawave/squads/${uuid}`);
return response.data;
},
createSquad: async (data: { name: string; inbound_uuids?: string[] }): Promise<SquadOperationResponse> => {
const response = await apiClient.post('/cabinet/admin/remnawave/squads', data)
return response.data
createSquad: async (data: {
name: string;
inbound_uuids?: string[];
}): Promise<SquadOperationResponse> => {
const response = await apiClient.post('/cabinet/admin/remnawave/squads', data);
return response.data;
},
updateSquad: async (uuid: string, data: { name?: string; inbound_uuids?: string[] }): Promise<SquadOperationResponse> => {
const response = await apiClient.patch(`/cabinet/admin/remnawave/squads/${uuid}`, data)
return response.data
updateSquad: async (
uuid: string,
data: { name?: string; inbound_uuids?: string[] },
): Promise<SquadOperationResponse> => {
const response = await apiClient.patch(`/cabinet/admin/remnawave/squads/${uuid}`, data);
return response.data;
},
deleteSquad: async (uuid: string): Promise<SquadOperationResponse> => {
const response = await apiClient.delete(`/cabinet/admin/remnawave/squads/${uuid}`)
return response.data
const response = await apiClient.delete(`/cabinet/admin/remnawave/squads/${uuid}`);
return response.data;
},
squadAction: async (uuid: string, data: {
action: 'add_all_users' | 'remove_all_users' | 'delete' | 'rename' | 'update_inbounds'
name?: string
inbound_uuids?: string[]
}): Promise<SquadOperationResponse> => {
const response = await apiClient.post(`/cabinet/admin/remnawave/squads/${uuid}/action`, data)
return response.data
squadAction: async (
uuid: string,
data: {
action: 'add_all_users' | 'remove_all_users' | 'delete' | 'rename' | 'update_inbounds';
name?: string;
inbound_uuids?: string[];
},
): Promise<SquadOperationResponse> => {
const response = await apiClient.post(`/cabinet/admin/remnawave/squads/${uuid}/action`, data);
return response.data;
},
// Migration
getMigrationPreview: async (uuid: string): Promise<MigrationPreviewResponse> => {
const response = await apiClient.get(`/cabinet/admin/remnawave/squads/${uuid}/migration-preview`)
return response.data
const response = await apiClient.get(
`/cabinet/admin/remnawave/squads/${uuid}/migration-preview`,
);
return response.data;
},
migrateSquad: async (sourceUuid: string, targetUuid: string): Promise<MigrationResponse> => {
const response = await apiClient.post('/cabinet/admin/remnawave/squads/migrate', {
source_uuid: sourceUuid,
target_uuid: targetUuid,
})
return response.data
});
return response.data;
},
// Inbounds
getInbounds: async (): Promise<InboundsListResponse> => {
const response = await apiClient.get('/cabinet/admin/remnawave/inbounds')
return response.data
const response = await apiClient.get('/cabinet/admin/remnawave/inbounds');
return response.data;
},
// Auto Sync
getAutoSyncStatus: async (): Promise<AutoSyncStatus> => {
const response = await apiClient.get('/cabinet/admin/remnawave/sync/auto/status')
return response.data
const response = await apiClient.get('/cabinet/admin/remnawave/sync/auto/status');
return response.data;
},
toggleAutoSync: async (enabled: boolean): Promise<SyncResponse> => {
const response = await apiClient.post('/cabinet/admin/remnawave/sync/auto/toggle', { enabled })
return response.data
const response = await apiClient.post('/cabinet/admin/remnawave/sync/auto/toggle', { enabled });
return response.data;
},
runAutoSync: async (): Promise<AutoSyncRunResponse> => {
const response = await apiClient.post('/cabinet/admin/remnawave/sync/auto/run')
return response.data
const response = await apiClient.post('/cabinet/admin/remnawave/sync/auto/run');
return response.data;
},
// Manual Sync
syncFromPanel: async (mode: 'all' | 'new_only' | 'update_only' = 'all'): Promise<SyncResponse> => {
const response = await apiClient.post('/cabinet/admin/remnawave/sync/from-panel', { mode })
return response.data
syncFromPanel: async (
mode: 'all' | 'new_only' | 'update_only' = 'all',
): Promise<SyncResponse> => {
const response = await apiClient.post('/cabinet/admin/remnawave/sync/from-panel', { mode });
return response.data;
},
syncToPanel: async (): Promise<SyncResponse> => {
const response = await apiClient.post('/cabinet/admin/remnawave/sync/to-panel')
return response.data
const response = await apiClient.post('/cabinet/admin/remnawave/sync/to-panel');
return response.data;
},
syncServers: async (): Promise<SyncResponse> => {
const response = await apiClient.post('/cabinet/admin/remnawave/sync/servers')
return response.data
const response = await apiClient.post('/cabinet/admin/remnawave/sync/servers');
return response.data;
},
validateSubscriptions: async (): Promise<SyncResponse> => {
const response = await apiClient.post('/cabinet/admin/remnawave/sync/subscriptions/validate')
return response.data
const response = await apiClient.post('/cabinet/admin/remnawave/sync/subscriptions/validate');
return response.data;
},
cleanupSubscriptions: async (): Promise<SyncResponse> => {
const response = await apiClient.post('/cabinet/admin/remnawave/sync/subscriptions/cleanup')
return response.data
const response = await apiClient.post('/cabinet/admin/remnawave/sync/subscriptions/cleanup');
return response.data;
},
syncSubscriptionStatuses: async (): Promise<SyncResponse> => {
const response = await apiClient.post('/cabinet/admin/remnawave/sync/subscriptions/statuses')
return response.data
const response = await apiClient.post('/cabinet/admin/remnawave/sync/subscriptions/statuses');
return response.data;
},
getSyncRecommendations: async (): Promise<SyncResponse> => {
const response = await apiClient.get('/cabinet/admin/remnawave/sync/recommendations')
return response.data
const response = await apiClient.get('/cabinet/admin/remnawave/sync/recommendations');
return response.data;
},
}
};
export default adminRemnawaveApi
export default adminRemnawaveApi;

View File

@@ -1,73 +1,79 @@
import apiClient from './client'
import apiClient from './client';
export interface SettingCategoryRef {
key: string
label: string
key: string;
label: string;
}
export interface SettingCategorySummary {
key: string
label: string
description: string
items: number
key: string;
label: string;
description: string;
items: number;
}
export interface SettingChoice {
value: unknown
label: string
description?: string | null
value: unknown;
label: string;
description?: string | null;
}
export interface SettingHint {
description: string
format: string
example: string
warning: string
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
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
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
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
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
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
const response = await apiClient.delete<SettingDefinition>(`/cabinet/admin/settings/${key}`);
return response.data;
},
}
};

View File

@@ -1,418 +1,478 @@
import apiClient from './client'
import apiClient from './client';
// ============ Types ============
export interface UserSubscriptionInfo {
id: number
status: string
is_trial: boolean
start_date: string | null
end_date: string | null
traffic_limit_gb: number
traffic_used_gb: number
device_limit: number
tariff_id: number | null
tariff_name: string | null
autopay_enabled: boolean
is_active: boolean
days_remaining: number
id: number;
status: string;
is_trial: boolean;
start_date: string | null;
end_date: string | null;
traffic_limit_gb: number;
traffic_used_gb: number;
device_limit: number;
tariff_id: number | null;
tariff_name: string | null;
autopay_enabled: boolean;
is_active: boolean;
days_remaining: number;
}
export interface UserPromoGroupInfo {
id: number
name: string
is_default: boolean
id: number;
name: string;
is_default: boolean;
}
export interface UserListItem {
id: number
telegram_id: number
username: string | null
first_name: string | null
last_name: string | null
full_name: string
status: string
balance_kopeks: number
balance_rubles: number
created_at: string
last_activity: string | null
has_subscription: boolean
subscription_status: string | null
subscription_is_trial: boolean
subscription_end_date: string | null
promo_group_id: number | null
promo_group_name: string | null
total_spent_kopeks: number
purchase_count: number
has_restrictions: boolean
restriction_topup: boolean
restriction_subscription: boolean
id: number;
telegram_id: number;
username: string | null;
first_name: string | null;
last_name: string | null;
full_name: string;
status: string;
balance_kopeks: number;
balance_rubles: number;
created_at: string;
last_activity: string | null;
has_subscription: boolean;
subscription_status: string | null;
subscription_is_trial: boolean;
subscription_end_date: string | null;
promo_group_id: number | null;
promo_group_name: string | null;
total_spent_kopeks: number;
purchase_count: number;
has_restrictions: boolean;
restriction_topup: boolean;
restriction_subscription: boolean;
}
export interface UsersListResponse {
users: UserListItem[]
total: number
offset: number
limit: number
users: UserListItem[];
total: number;
offset: number;
limit: number;
}
export interface UserTransactionItem {
id: number
type: string
amount_kopeks: number
amount_rubles: number
description: string | null
payment_method: string | null
is_completed: boolean
created_at: string
id: number;
type: string;
amount_kopeks: number;
amount_rubles: number;
description: string | null;
payment_method: string | null;
is_completed: boolean;
created_at: string;
}
export interface UserReferralInfo {
referral_code: string
referrals_count: number
total_earnings_kopeks: number
commission_percent: number | null
referred_by_id: number | null
referred_by_username: string | null
referral_code: string;
referrals_count: number;
total_earnings_kopeks: number;
commission_percent: number | null;
referred_by_id: number | null;
referred_by_username: string | null;
}
export interface UserDetailResponse {
id: number
telegram_id: number
username: string | null
first_name: string | null
last_name: string | null
full_name: string
status: string
language: string
balance_kopeks: number
balance_rubles: number
email: string | null
email_verified: boolean
created_at: string
updated_at: string | null
last_activity: string | null
cabinet_last_login: string | null
subscription: UserSubscriptionInfo | null
promo_group: UserPromoGroupInfo | null
referral: UserReferralInfo
total_spent_kopeks: number
purchase_count: number
used_promocodes: number
has_had_paid_subscription: boolean
lifetime_used_traffic_bytes: number
restriction_topup: boolean
restriction_subscription: boolean
restriction_reason: string | null
promo_offer_discount_percent: number
promo_offer_discount_source: string | null
promo_offer_discount_expires_at: string | null
recent_transactions: UserTransactionItem[]
remnawave_uuid: string | null
id: number;
telegram_id: number;
username: string | null;
first_name: string | null;
last_name: string | null;
full_name: string;
status: string;
language: string;
balance_kopeks: number;
balance_rubles: number;
email: string | null;
email_verified: boolean;
created_at: string;
updated_at: string | null;
last_activity: string | null;
cabinet_last_login: string | null;
subscription: UserSubscriptionInfo | null;
promo_group: UserPromoGroupInfo | null;
referral: UserReferralInfo;
total_spent_kopeks: number;
purchase_count: number;
used_promocodes: number;
has_had_paid_subscription: boolean;
lifetime_used_traffic_bytes: number;
restriction_topup: boolean;
restriction_subscription: boolean;
restriction_reason: string | null;
promo_offer_discount_percent: number;
promo_offer_discount_source: string | null;
promo_offer_discount_expires_at: string | null;
recent_transactions: UserTransactionItem[];
remnawave_uuid: string | null;
}
export interface UsersStatsResponse {
total_users: number
active_users: number
blocked_users: number
deleted_users: number
new_today: number
new_week: number
new_month: number
users_with_subscription: number
users_with_active_subscription: number
users_with_trial: number
users_with_expired_subscription: number
total_balance_kopeks: number
total_balance_rubles: number
avg_balance_kopeks: number
active_today: number
active_week: number
active_month: number
total_users: number;
active_users: number;
blocked_users: number;
deleted_users: number;
new_today: number;
new_week: number;
new_month: number;
users_with_subscription: number;
users_with_active_subscription: number;
users_with_trial: number;
users_with_expired_subscription: number;
total_balance_kopeks: number;
total_balance_rubles: number;
avg_balance_kopeks: number;
active_today: number;
active_week: number;
active_month: number;
}
// Available tariffs
export interface PeriodPriceInfo {
days: number
price_kopeks: number
price_rubles: number
days: number;
price_kopeks: number;
price_rubles: number;
}
export interface UserAvailableTariff {
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
period_prices: PeriodPriceInfo[]
is_daily: boolean
daily_price_kopeks: number
custom_days_enabled: boolean
price_per_day_kopeks: number
min_days: number
max_days: number
is_available: boolean
requires_promo_group: boolean
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;
period_prices: PeriodPriceInfo[];
is_daily: boolean;
daily_price_kopeks: number;
custom_days_enabled: boolean;
price_per_day_kopeks: number;
min_days: number;
max_days: number;
is_available: boolean;
requires_promo_group: boolean;
}
export interface UserAvailableTariffsResponse {
user_id: number
promo_group_id: number | null
promo_group_name: string | null
tariffs: UserAvailableTariff[]
total: number
current_tariff_id: number | null
current_tariff_name: string | null
user_id: number;
promo_group_id: number | null;
promo_group_name: string | null;
tariffs: UserAvailableTariff[];
total: number;
current_tariff_id: number | null;
current_tariff_name: string | null;
}
// Sync types
export interface PanelUserInfo {
uuid: string | null
short_uuid: string | null
username: string | null
status: string | null
expire_at: string | null
traffic_limit_gb: number
traffic_used_gb: number
device_limit: number
subscription_url: string | null
active_squads: string[]
uuid: string | null;
short_uuid: string | null;
username: string | null;
status: string | null;
expire_at: string | null;
traffic_limit_gb: number;
traffic_used_gb: number;
device_limit: number;
subscription_url: string | null;
active_squads: string[];
}
export interface SyncFromPanelResponse {
success: boolean
message: string
panel_user: PanelUserInfo | null
changes: Record<string, unknown>
errors: string[]
success: boolean;
message: string;
panel_user: PanelUserInfo | null;
changes: Record<string, unknown>;
errors: string[];
}
export interface SyncToPanelResponse {
success: boolean
message: string
action: string
panel_uuid: string | null
changes: Record<string, unknown>
errors: string[]
success: boolean;
message: string;
action: string;
panel_uuid: string | null;
changes: Record<string, unknown>;
errors: string[];
}
export interface PanelSyncStatusResponse {
user_id: number
telegram_id: number
remnawave_uuid: string | null
last_sync: string | null
bot_subscription_status: string | null
bot_subscription_end_date: string | null
bot_traffic_limit_gb: number
bot_traffic_used_gb: number
bot_device_limit: number
bot_squads: string[]
panel_found: boolean
panel_status: string | null
panel_expire_at: string | null
panel_traffic_limit_gb: number
panel_traffic_used_gb: number
panel_device_limit: number
panel_squads: string[]
has_differences: boolean
differences: string[]
user_id: number;
telegram_id: number;
remnawave_uuid: string | null;
last_sync: string | null;
bot_subscription_status: string | null;
bot_subscription_end_date: string | null;
bot_traffic_limit_gb: number;
bot_traffic_used_gb: number;
bot_device_limit: number;
bot_squads: string[];
panel_found: boolean;
panel_status: string | null;
panel_expire_at: string | null;
panel_traffic_limit_gb: number;
panel_traffic_used_gb: number;
panel_device_limit: number;
panel_squads: string[];
has_differences: boolean;
differences: string[];
}
// Update types
export interface UpdateBalanceRequest {
amount_kopeks: number
description?: string
create_transaction?: boolean
amount_kopeks: number;
description?: string;
create_transaction?: boolean;
}
export interface UpdateBalanceResponse {
success: boolean
old_balance_kopeks: number
new_balance_kopeks: number
message: string
success: boolean;
old_balance_kopeks: number;
new_balance_kopeks: number;
message: string;
}
export interface UpdateSubscriptionRequest {
action: 'extend' | 'set_end_date' | 'change_tariff' | 'set_traffic' | 'toggle_autopay' | 'cancel' | 'activate' | 'create'
days?: number
end_date?: string
tariff_id?: number
traffic_limit_gb?: number
traffic_used_gb?: number
autopay_enabled?: boolean
is_trial?: boolean
device_limit?: number
action:
| 'extend'
| 'set_end_date'
| 'change_tariff'
| 'set_traffic'
| 'toggle_autopay'
| 'cancel'
| 'activate'
| 'create';
days?: number;
end_date?: string;
tariff_id?: number;
traffic_limit_gb?: number;
traffic_used_gb?: number;
autopay_enabled?: boolean;
is_trial?: boolean;
device_limit?: number;
}
export interface UpdateSubscriptionResponse {
success: boolean
message: string
subscription: UserSubscriptionInfo | null
success: boolean;
message: string;
subscription: UserSubscriptionInfo | null;
}
export interface UpdateUserStatusResponse {
success: boolean
old_status: string
new_status: string
message: string
success: boolean;
old_status: string;
new_status: string;
message: string;
}
export interface UpdateRestrictionsRequest {
restriction_topup?: boolean
restriction_subscription?: boolean
restriction_reason?: string
restriction_topup?: boolean;
restriction_subscription?: boolean;
restriction_reason?: string;
}
export interface UpdateRestrictionsResponse {
success: boolean
restriction_topup: boolean
restriction_subscription: boolean
restriction_reason: string | null
message: string
success: boolean;
restriction_topup: boolean;
restriction_subscription: boolean;
restriction_reason: string | null;
message: string;
}
export interface SyncFromPanelRequest {
update_subscription?: boolean
update_traffic?: boolean
create_if_missing?: boolean
update_subscription?: boolean;
update_traffic?: boolean;
create_if_missing?: boolean;
}
export interface SyncToPanelRequest {
create_if_missing?: boolean
update_status?: boolean
update_traffic_limit?: boolean
update_expire_date?: boolean
update_squads?: boolean
create_if_missing?: boolean;
update_status?: boolean;
update_traffic_limit?: boolean;
update_expire_date?: boolean;
update_squads?: boolean;
}
// ============ API ============
export const adminUsersApi = {
// List users
getUsers: async (params: {
offset?: number
limit?: number
search?: string
status?: 'active' | 'blocked' | 'deleted'
sort_by?: 'created_at' | 'balance' | 'traffic' | 'last_activity' | 'total_spent' | 'purchase_count'
} = {}): Promise<UsersListResponse> => {
const response = await apiClient.get('/cabinet/admin/users', { params })
return response.data
getUsers: async (
params: {
offset?: number;
limit?: number;
search?: string;
status?: 'active' | 'blocked' | 'deleted';
sort_by?:
| 'created_at'
| 'balance'
| 'traffic'
| 'last_activity'
| 'total_spent'
| 'purchase_count';
} = {},
): Promise<UsersListResponse> => {
const response = await apiClient.get('/cabinet/admin/users', { params });
return response.data;
},
// Get users stats
getStats: async (): Promise<UsersStatsResponse> => {
const response = await apiClient.get('/cabinet/admin/users/stats')
return response.data
const response = await apiClient.get('/cabinet/admin/users/stats');
return response.data;
},
// Get user detail
getUser: async (userId: number): Promise<UserDetailResponse> => {
const response = await apiClient.get(`/cabinet/admin/users/${userId}`)
return response.data
const response = await apiClient.get(`/cabinet/admin/users/${userId}`);
return response.data;
},
// Get user by telegram ID
getUserByTelegram: async (telegramId: number): Promise<UserDetailResponse> => {
const response = await apiClient.get(`/cabinet/admin/users/by-telegram/${telegramId}`)
return response.data
const response = await apiClient.get(`/cabinet/admin/users/by-telegram/${telegramId}`);
return response.data;
},
// Get available tariffs for user
getAvailableTariffs: async (userId: number, includeInactive = false): Promise<UserAvailableTariffsResponse> => {
getAvailableTariffs: async (
userId: number,
includeInactive = false,
): Promise<UserAvailableTariffsResponse> => {
const response = await apiClient.get(`/cabinet/admin/users/${userId}/available-tariffs`, {
params: { include_inactive: includeInactive }
})
return response.data
params: { include_inactive: includeInactive },
});
return response.data;
},
// Update balance
updateBalance: async (userId: number, data: UpdateBalanceRequest): Promise<UpdateBalanceResponse> => {
const response = await apiClient.post(`/cabinet/admin/users/${userId}/balance`, data)
return response.data
updateBalance: async (
userId: number,
data: UpdateBalanceRequest,
): Promise<UpdateBalanceResponse> => {
const response = await apiClient.post(`/cabinet/admin/users/${userId}/balance`, data);
return response.data;
},
// Update subscription
updateSubscription: async (userId: number, data: UpdateSubscriptionRequest): Promise<UpdateSubscriptionResponse> => {
const response = await apiClient.post(`/cabinet/admin/users/${userId}/subscription`, data)
return response.data
updateSubscription: async (
userId: number,
data: UpdateSubscriptionRequest,
): Promise<UpdateSubscriptionResponse> => {
const response = await apiClient.post(`/cabinet/admin/users/${userId}/subscription`, data);
return response.data;
},
// Update status
updateStatus: async (userId: number, status: 'active' | 'blocked' | 'deleted', reason?: string): Promise<UpdateUserStatusResponse> => {
const response = await apiClient.post(`/cabinet/admin/users/${userId}/status`, { status, reason })
return response.data
updateStatus: async (
userId: number,
status: 'active' | 'blocked' | 'deleted',
reason?: string,
): Promise<UpdateUserStatusResponse> => {
const response = await apiClient.post(`/cabinet/admin/users/${userId}/status`, {
status,
reason,
});
return response.data;
},
// Block user
blockUser: async (userId: number, reason?: string): Promise<UpdateUserStatusResponse> => {
const response = await apiClient.post(`/cabinet/admin/users/${userId}/block`, null, { params: { reason } })
return response.data
const response = await apiClient.post(`/cabinet/admin/users/${userId}/block`, null, {
params: { reason },
});
return response.data;
},
// Unblock user
unblockUser: async (userId: number): Promise<UpdateUserStatusResponse> => {
const response = await apiClient.post(`/cabinet/admin/users/${userId}/unblock`)
return response.data
const response = await apiClient.post(`/cabinet/admin/users/${userId}/unblock`);
return response.data;
},
// Update restrictions
updateRestrictions: async (userId: number, data: UpdateRestrictionsRequest): Promise<UpdateRestrictionsResponse> => {
const response = await apiClient.post(`/cabinet/admin/users/${userId}/restrictions`, data)
return response.data
updateRestrictions: async (
userId: number,
data: UpdateRestrictionsRequest,
): Promise<UpdateRestrictionsResponse> => {
const response = await apiClient.post(`/cabinet/admin/users/${userId}/restrictions`, data);
return response.data;
},
// Update promo group
updatePromoGroup: async (userId: number, promoGroupId: number | null): Promise<{ success: boolean; message: string }> => {
const response = await apiClient.post(`/cabinet/admin/users/${userId}/promo-group`, { promo_group_id: promoGroupId })
return response.data
updatePromoGroup: async (
userId: number,
promoGroupId: number | null,
): Promise<{ success: boolean; message: string }> => {
const response = await apiClient.post(`/cabinet/admin/users/${userId}/promo-group`, {
promo_group_id: promoGroupId,
});
return response.data;
},
// Delete user
deleteUser: async (userId: number, softDelete = true, reason?: string): Promise<{ success: boolean; message: string }> => {
deleteUser: async (
userId: number,
softDelete = true,
reason?: string,
): Promise<{ success: boolean; message: string }> => {
const response = await apiClient.delete(`/cabinet/admin/users/${userId}`, {
data: { soft_delete: softDelete, reason }
})
return response.data
data: { soft_delete: softDelete, reason },
});
return response.data;
},
// Get referrals
getReferrals: async (userId: number, offset = 0, limit = 50): Promise<UsersListResponse> => {
const response = await apiClient.get(`/cabinet/admin/users/${userId}/referrals`, {
params: { offset, limit }
})
return response.data
params: { offset, limit },
});
return response.data;
},
// Get transactions
getTransactions: async (userId: number, params: {
offset?: number
limit?: number
transaction_type?: string
} = {}): Promise<{ transactions: UserTransactionItem[]; total: number; offset: number; limit: number }> => {
const response = await apiClient.get(`/cabinet/admin/users/${userId}/transactions`, { params })
return response.data
getTransactions: async (
userId: number,
params: {
offset?: number;
limit?: number;
transaction_type?: string;
} = {},
): Promise<{
transactions: UserTransactionItem[];
total: number;
offset: number;
limit: number;
}> => {
const response = await apiClient.get(`/cabinet/admin/users/${userId}/transactions`, { params });
return response.data;
},
// Sync status
getSyncStatus: async (userId: number): Promise<PanelSyncStatusResponse> => {
const response = await apiClient.get(`/cabinet/admin/users/${userId}/sync/status`)
return response.data
const response = await apiClient.get(`/cabinet/admin/users/${userId}/sync/status`);
return response.data;
},
// Sync from panel
syncFromPanel: async (userId: number, data: SyncFromPanelRequest = {}): Promise<SyncFromPanelResponse> => {
const response = await apiClient.post(`/cabinet/admin/users/${userId}/sync/from-panel`, data)
return response.data
syncFromPanel: async (
userId: number,
data: SyncFromPanelRequest = {},
): Promise<SyncFromPanelResponse> => {
const response = await apiClient.post(`/cabinet/admin/users/${userId}/sync/from-panel`, data);
return response.data;
},
// Sync to panel
syncToPanel: async (userId: number, data: SyncToPanelRequest = {}): Promise<SyncToPanelResponse> => {
const response = await apiClient.post(`/cabinet/admin/users/${userId}/sync/to-panel`, data)
return response.data
syncToPanel: async (
userId: number,
data: SyncToPanelRequest = {},
): Promise<SyncToPanelResponse> => {
const response = await apiClient.post(`/cabinet/admin/users/${userId}/sync/to-panel`, data);
return response.data;
},
}
};

View File

@@ -1,27 +1,27 @@
import apiClient from './client'
import type { AuthResponse, RegisterResponse, TokenResponse, User } from '../types'
import apiClient from './client';
import type { AuthResponse, RegisterResponse, 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
});
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
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
const response = await apiClient.post<AuthResponse>('/cabinet/auth/telegram/widget', data);
return response.data;
},
// Email login
@@ -29,63 +29,69 @@ export const authApi = {
const response = await apiClient.post<AuthResponse>('/cabinet/auth/email/login', {
email,
password,
})
return response.data
});
return response.data;
},
// Register email (link to existing Telegram account)
registerEmail: async (email: string, password: string): Promise<{ message: string; email: string }> => {
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
});
return response.data;
},
// Register standalone email account (no Telegram required)
// Returns message - user must verify email before login
registerEmailStandalone: async (data: {
email: string
password: string
first_name?: string
language?: string
referral_code?: string
email: string;
password: string;
first_name?: string;
language?: string;
referral_code?: string;
}): Promise<RegisterResponse> => {
const response = await apiClient.post<RegisterResponse>('/cabinet/auth/email/register/standalone', data)
return response.data
const response = await apiClient.post<RegisterResponse>(
'/cabinet/auth/email/register/standalone',
data,
);
return response.data;
},
// Verify email and get auth tokens
verifyEmail: async (token: string): Promise<AuthResponse> => {
const response = await apiClient.post<AuthResponse>('/cabinet/auth/email/verify', { token })
return response.data
const response = await apiClient.post<AuthResponse>('/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
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
});
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
const response = await apiClient.post('/cabinet/auth/password/forgot', { email });
return response.data;
},
// Reset password
@@ -93,13 +99,13 @@ export const authApi = {
const response = await apiClient.post('/cabinet/auth/password/reset', {
token,
password,
})
return response.data
});
return response.data;
},
// Get current user
getMe: async (): Promise<User> => {
const response = await apiClient.get<User>('/cabinet/auth/me')
return response.data
const response = await apiClient.get<User>('/cabinet/auth/me');
return response.data;
},
}
};

View File

@@ -1,100 +1,124 @@
import apiClient from './client'
import type { Balance, Transaction, PaymentMethod, PaginatedResponse, PendingPayment, ManualCheckResponse } from '../types'
import apiClient from './client';
import type {
Balance,
Transaction,
PaymentMethod,
PaginatedResponse,
PendingPayment,
ManualCheckResponse,
} from '../types';
export const balanceApi = {
// Get current balance
getBalance: async (): Promise<Balance> => {
const response = await apiClient.get<Balance>('/cabinet/balance')
return response.data
const response = await apiClient.get<Balance>('/cabinet/balance');
return response.data;
},
// Get transaction history
getTransactions: async (params?: {
page?: number
per_page?: number
type?: string
page?: number;
per_page?: number;
type?: string;
}): Promise<PaginatedResponse<Transaction>> => {
const response = await apiClient.get<PaginatedResponse<Transaction>>('/cabinet/balance/transactions', {
params,
})
return response.data
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
const response = await apiClient.get<PaymentMethod[]>('/cabinet/balance/payment-methods');
return response.data;
},
// Create top-up payment
createTopUp: async (amountKopeks: number, paymentMethod: string, paymentOption?: string): Promise<{
payment_id: string
payment_url: string
amount_kopeks: number
amount_rubles: number
status: string
expires_at: string | null
createTopUp: async (
amountKopeks: number,
paymentMethod: string,
paymentOption?: string,
): Promise<{
payment_id: string;
payment_url: string;
amount_kopeks: number;
amount_rubles: number;
status: string;
expires_at: string | null;
}> => {
const payload: {
amount_kopeks: number
payment_method: string
payment_option?: string
amount_kopeks: number;
payment_method: string;
payment_option?: string;
} = {
amount_kopeks: amountKopeks,
payment_method: paymentMethod,
}
};
if (paymentOption) {
payload.payment_option = paymentOption
payload.payment_option = paymentOption;
}
const response = await apiClient.post('/cabinet/balance/topup', payload)
return response.data
const response = await apiClient.post('/cabinet/balance/topup', payload);
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
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
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
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
});
return response.data;
},
// Get pending payments for manual verification
getPendingPayments: async (params?: {
page?: number
per_page?: number
page?: number;
per_page?: number;
}): Promise<PaginatedResponse<PendingPayment>> => {
const response = await apiClient.get<PaginatedResponse<PendingPayment>>('/cabinet/balance/pending-payments', {
params,
})
return response.data
const response = await apiClient.get<PaginatedResponse<PendingPayment>>(
'/cabinet/balance/pending-payments',
{
params,
},
);
return response.data;
},
// Get specific pending payment details
getPendingPayment: async (method: string, paymentId: number): Promise<PendingPayment> => {
const response = await apiClient.get<PendingPayment>(`/cabinet/balance/pending-payments/${method}/${paymentId}`)
return response.data
const response = await apiClient.get<PendingPayment>(
`/cabinet/balance/pending-payments/${method}/${paymentId}`,
);
return response.data;
},
// Manually check payment status
checkPaymentStatus: async (method: string, paymentId: number): Promise<ManualCheckResponse> => {
const response = await apiClient.post<ManualCheckResponse>(`/cabinet/balance/pending-payments/${method}/${paymentId}/check`)
return response.data
const response = await apiClient.post<ManualCheckResponse>(
`/cabinet/balance/pending-payments/${method}/${paymentId}/check`,
);
return response.data;
},
}
};

View File

@@ -1,270 +1,270 @@
import apiClient from './client'
import apiClient from './client';
// === Types ===
export interface BanSystemStatus {
enabled: boolean
configured: boolean
enabled: boolean;
configured: boolean;
}
export interface BanSystemStats {
total_users: number
active_users: number
users_over_limit: number
total_requests: number
total_punishments: number
active_punishments: number
nodes_online: number
nodes_total: number
agents_online: number
agents_total: number
panel_connected: boolean
uptime_seconds: number | null
total_users: number;
active_users: number;
users_over_limit: number;
total_requests: number;
total_punishments: number;
active_punishments: number;
nodes_online: number;
nodes_total: number;
agents_online: number;
agents_total: number;
panel_connected: boolean;
uptime_seconds: number | null;
}
export interface BanUserIPInfo {
ip: string
first_seen: string | null
last_seen: string | null
node: string | null
request_count: number
country_code: string | null
country_name: string | null
city: string | null
ip: string;
first_seen: string | null;
last_seen: string | null;
node: string | null;
request_count: number;
country_code: string | null;
country_name: string | null;
city: string | null;
}
export interface BanUserRequestLog {
timestamp: string
source_ip: string
destination: string | null
dest_port: number | null
protocol: string | null
action: string | null
node: string | null
timestamp: string;
source_ip: string;
destination: string | null;
dest_port: number | null;
protocol: string | null;
action: string | null;
node: string | null;
}
export interface BanUserListItem {
email: string
unique_ip_count: number
total_requests: number
limit: number | null
is_over_limit: boolean
blocked_count: number
last_seen: string | null
email: string;
unique_ip_count: number;
total_requests: number;
limit: number | null;
is_over_limit: boolean;
blocked_count: number;
last_seen: string | null;
}
export interface BanUsersListResponse {
users: BanUserListItem[]
total: number
offset: number
limit: number
users: BanUserListItem[];
total: number;
offset: number;
limit: number;
}
export interface BanUserDetailResponse {
email: string
unique_ip_count: number
total_requests: number
limit: number | null
is_over_limit: boolean
blocked_count: number
ips: BanUserIPInfo[]
recent_requests: BanUserRequestLog[]
network_type: string | null
email: string;
unique_ip_count: number;
total_requests: number;
limit: number | null;
is_over_limit: boolean;
blocked_count: number;
ips: BanUserIPInfo[];
recent_requests: BanUserRequestLog[];
network_type: string | null;
}
export interface BanPunishmentItem {
id: number | null
user_id: string
uuid: string | null
username: string
reason: string | null
punished_at: string
enable_at: string | null
ip_count: number
limit: number
enabled: boolean
enabled_at: string | null
node_name: string | null
id: number | null;
user_id: string;
uuid: string | null;
username: string;
reason: string | null;
punished_at: string;
enable_at: string | null;
ip_count: number;
limit: number;
enabled: boolean;
enabled_at: string | null;
node_name: string | null;
}
export interface BanPunishmentsListResponse {
punishments: BanPunishmentItem[]
total: number
punishments: BanPunishmentItem[];
total: number;
}
export interface BanHistoryResponse {
items: BanPunishmentItem[]
total: number
items: BanPunishmentItem[];
total: number;
}
export interface BanUserRequest {
username: string
minutes: number
reason?: string
username: string;
minutes: number;
reason?: string;
}
export interface UnbanResponse {
success: boolean
message: string
success: boolean;
message: string;
}
export interface BanNodeItem {
name: string
address: string | null
is_connected: boolean
last_seen: string | null
users_count: number
agent_stats: Record<string, unknown> | null
name: string;
address: string | null;
is_connected: boolean;
last_seen: string | null;
users_count: number;
agent_stats: Record<string, unknown> | null;
}
export interface BanNodesListResponse {
nodes: BanNodeItem[]
total: number
online: number
nodes: BanNodeItem[];
total: number;
online: number;
}
export interface BanAgentItem {
node_name: string
sent_total: number
dropped_total: number
batches_total: number
reconnects: number
failures: number
queue_size: number
queue_max: number
dedup_checked: number
dedup_skipped: number
filter_checked: number
filter_filtered: number
health: string
is_online: boolean
last_report: string | null
node_name: string;
sent_total: number;
dropped_total: number;
batches_total: number;
reconnects: number;
failures: number;
queue_size: number;
queue_max: number;
dedup_checked: number;
dedup_skipped: number;
filter_checked: number;
filter_filtered: number;
health: string;
is_online: boolean;
last_report: string | null;
}
export interface BanAgentsSummary {
total_agents: number
online_agents: number
total_sent: number
total_dropped: number
avg_queue_size: number
healthy_count: number
warning_count: number
critical_count: number
total_agents: number;
online_agents: number;
total_sent: number;
total_dropped: number;
avg_queue_size: number;
healthy_count: number;
warning_count: number;
critical_count: number;
}
export interface BanAgentsListResponse {
agents: BanAgentItem[]
summary: BanAgentsSummary | null
total: number
online: number
agents: BanAgentItem[];
summary: BanAgentsSummary | null;
total: number;
online: number;
}
export interface BanTrafficViolationItem {
id: number | null
username: string
email: string | null
violation_type: string
description: string | null
bytes_used: number
bytes_limit: number
detected_at: string
resolved: boolean
id: number | null;
username: string;
email: string | null;
violation_type: string;
description: string | null;
bytes_used: number;
bytes_limit: number;
detected_at: string;
resolved: boolean;
}
export interface BanTrafficViolationsResponse {
violations: BanTrafficViolationItem[]
total: number
violations: BanTrafficViolationItem[];
total: number;
}
export interface BanTrafficTopItem {
username: string
bytes_total: number
bytes_limit: number | null
over_limit: boolean
username: string;
bytes_total: number;
bytes_limit: number | null;
over_limit: boolean;
}
export interface BanTrafficResponse {
enabled: boolean
stats: Record<string, unknown> | null
top_users: BanTrafficTopItem[]
recent_violations: BanTrafficViolationItem[]
enabled: boolean;
stats: Record<string, unknown> | null;
top_users: BanTrafficTopItem[];
recent_violations: BanTrafficViolationItem[];
}
// === Settings Types ===
export interface BanSettingDefinition {
key: string
value: unknown
type: string
min_value: number | null
max_value: number | null
editable: boolean
description: string | null
category: string | null
key: string;
value: unknown;
type: string;
min_value: number | null;
max_value: number | null;
editable: boolean;
description: string | null;
category: string | null;
}
export interface BanSettingsResponse {
settings: BanSettingDefinition[]
settings: BanSettingDefinition[];
}
export interface BanWhitelistRequest {
username: string
username: string;
}
// === Report Types ===
export interface BanReportTopViolator {
username: string
count: number
username: string;
count: number;
}
export interface BanReportResponse {
period_hours: number
current_users: number
current_ips: number
punishment_stats: Record<string, unknown> | null
top_violators: BanReportTopViolator[]
period_hours: number;
current_users: number;
current_ips: number;
punishment_stats: Record<string, unknown> | null;
top_violators: BanReportTopViolator[];
}
// === Health Types ===
export interface BanHealthComponent {
name: string
status: string
message: string | null
details: Record<string, unknown> | null
name: string;
status: string;
message: string | null;
details: Record<string, unknown> | null;
}
export interface BanHealthResponse {
status: string
uptime: number | null
components: BanHealthComponent[]
status: string;
uptime: number | null;
components: BanHealthComponent[];
}
export interface BanHealthDetailedResponse {
status: string
uptime: number | null
components: Record<string, unknown>
status: string;
uptime: number | null;
components: Record<string, unknown>;
}
// === Agent History Types ===
export interface BanAgentHistoryItem {
timestamp: string
sent_total: number
dropped_total: number
queue_size: number
batches_total: number
timestamp: string;
sent_total: number;
dropped_total: number;
queue_size: number;
batches_total: number;
}
export interface BanAgentHistoryResponse {
node: string
hours: number
records: number
delta: Record<string, unknown> | null
first: Record<string, unknown> | null
last: Record<string, unknown> | null
history: BanAgentHistoryItem[]
node: string;
hours: number;
records: number;
delta: Record<string, unknown> | null;
first: Record<string, unknown> | null;
last: Record<string, unknown> | null;
history: BanAgentHistoryItem[];
}
// === API ===
@@ -272,174 +272,198 @@ export interface BanAgentHistoryResponse {
export const banSystemApi = {
// Status
getStatus: async (): Promise<BanSystemStatus> => {
const response = await apiClient.get('/cabinet/admin/ban-system/status')
return response.data
const response = await apiClient.get('/cabinet/admin/ban-system/status');
return response.data;
},
// Stats
getStats: async (): Promise<BanSystemStats> => {
const response = await apiClient.get('/cabinet/admin/ban-system/stats')
return response.data
const response = await apiClient.get('/cabinet/admin/ban-system/stats');
return response.data;
},
// Users
getUsers: async (params: {
offset?: number
limit?: number
status?: string
} = {}): Promise<BanUsersListResponse> => {
const response = await apiClient.get('/cabinet/admin/ban-system/users', { params })
return response.data
getUsers: async (
params: {
offset?: number;
limit?: number;
status?: string;
} = {},
): Promise<BanUsersListResponse> => {
const response = await apiClient.get('/cabinet/admin/ban-system/users', { params });
return response.data;
},
getUsersOverLimit: async (limit: number = 50): Promise<BanUsersListResponse> => {
const response = await apiClient.get('/cabinet/admin/ban-system/users/over-limit', {
params: { limit }
})
return response.data
params: { limit },
});
return response.data;
},
searchUsers: async (query: string): Promise<BanUsersListResponse> => {
const response = await apiClient.get(`/cabinet/admin/ban-system/users/search/${encodeURIComponent(query)}`)
return response.data
const response = await apiClient.get(
`/cabinet/admin/ban-system/users/search/${encodeURIComponent(query)}`,
);
return response.data;
},
getUser: async (email: string): Promise<BanUserDetailResponse> => {
const response = await apiClient.get(`/cabinet/admin/ban-system/users/${encodeURIComponent(email)}`)
return response.data
const response = await apiClient.get(
`/cabinet/admin/ban-system/users/${encodeURIComponent(email)}`,
);
return response.data;
},
// Punishments
getPunishments: async (): Promise<BanPunishmentsListResponse> => {
const response = await apiClient.get('/cabinet/admin/ban-system/punishments')
return response.data
const response = await apiClient.get('/cabinet/admin/ban-system/punishments');
return response.data;
},
unbanUser: async (userId: string): Promise<UnbanResponse> => {
const response = await apiClient.post(`/cabinet/admin/ban-system/punishments/${userId}/unban`)
return response.data
const response = await apiClient.post(`/cabinet/admin/ban-system/punishments/${userId}/unban`);
return response.data;
},
banUser: async (data: BanUserRequest): Promise<UnbanResponse> => {
const response = await apiClient.post('/cabinet/admin/ban-system/ban', data)
return response.data
const response = await apiClient.post('/cabinet/admin/ban-system/ban', data);
return response.data;
},
getPunishmentHistory: async (query: string, limit: number = 20): Promise<BanHistoryResponse> => {
const response = await apiClient.get(`/cabinet/admin/ban-system/history/${encodeURIComponent(query)}`, {
params: { limit }
})
return response.data
const response = await apiClient.get(
`/cabinet/admin/ban-system/history/${encodeURIComponent(query)}`,
{
params: { limit },
},
);
return response.data;
},
// Nodes
getNodes: async (): Promise<BanNodesListResponse> => {
const response = await apiClient.get('/cabinet/admin/ban-system/nodes')
return response.data
const response = await apiClient.get('/cabinet/admin/ban-system/nodes');
return response.data;
},
// Agents
getAgents: async (params: {
search?: string
health?: string
status?: string
} = {}): Promise<BanAgentsListResponse> => {
const response = await apiClient.get('/cabinet/admin/ban-system/agents', { params })
return response.data
getAgents: async (
params: {
search?: string;
health?: string;
status?: string;
} = {},
): Promise<BanAgentsListResponse> => {
const response = await apiClient.get('/cabinet/admin/ban-system/agents', { params });
return response.data;
},
getAgentsSummary: async (): Promise<BanAgentsSummary> => {
const response = await apiClient.get('/cabinet/admin/ban-system/agents/summary')
return response.data
const response = await apiClient.get('/cabinet/admin/ban-system/agents/summary');
return response.data;
},
// Traffic violations
getTrafficViolations: async (limit: number = 50): Promise<BanTrafficViolationsResponse> => {
const response = await apiClient.get('/cabinet/admin/ban-system/traffic/violations', {
params: { limit }
})
return response.data
params: { limit },
});
return response.data;
},
// Full Traffic
getTraffic: async (): Promise<BanTrafficResponse> => {
const response = await apiClient.get('/cabinet/admin/ban-system/traffic')
return response.data
const response = await apiClient.get('/cabinet/admin/ban-system/traffic');
return response.data;
},
getTrafficTop: async (limit: number = 20): Promise<BanTrafficTopItem[]> => {
const response = await apiClient.get('/cabinet/admin/ban-system/traffic/top', {
params: { limit }
})
return response.data
params: { limit },
});
return response.data;
},
// Settings
getSettings: async (): Promise<BanSettingsResponse> => {
const response = await apiClient.get('/cabinet/admin/ban-system/settings')
return response.data
const response = await apiClient.get('/cabinet/admin/ban-system/settings');
return response.data;
},
getSetting: async (key: string): Promise<BanSettingDefinition> => {
const response = await apiClient.get(`/cabinet/admin/ban-system/settings/${key}`)
return response.data
const response = await apiClient.get(`/cabinet/admin/ban-system/settings/${key}`);
return response.data;
},
setSetting: async (key: string, value: string): Promise<BanSettingDefinition> => {
const response = await apiClient.post(`/cabinet/admin/ban-system/settings/${key}`, null, {
params: { value }
})
return response.data
params: { value },
});
return response.data;
},
toggleSetting: async (key: string): Promise<BanSettingDefinition> => {
const response = await apiClient.post(`/cabinet/admin/ban-system/settings/${key}/toggle`)
return response.data
const response = await apiClient.post(`/cabinet/admin/ban-system/settings/${key}/toggle`);
return response.data;
},
// Whitelist
whitelistAdd: async (username: string): Promise<UnbanResponse> => {
const response = await apiClient.post('/cabinet/admin/ban-system/settings/whitelist/add', { username })
return response.data
const response = await apiClient.post('/cabinet/admin/ban-system/settings/whitelist/add', {
username,
});
return response.data;
},
whitelistRemove: async (username: string): Promise<UnbanResponse> => {
const response = await apiClient.post('/cabinet/admin/ban-system/settings/whitelist/remove', { username })
return response.data
const response = await apiClient.post('/cabinet/admin/ban-system/settings/whitelist/remove', {
username,
});
return response.data;
},
// Reports
getReport: async (hours: number = 24): Promise<BanReportResponse> => {
const response = await apiClient.get('/cabinet/admin/ban-system/report', {
params: { hours }
})
return response.data
params: { hours },
});
return response.data;
},
// Health
getHealth: async (): Promise<BanHealthResponse> => {
const response = await apiClient.get('/cabinet/admin/ban-system/health')
return response.data
const response = await apiClient.get('/cabinet/admin/ban-system/health');
return response.data;
},
getHealthDetailed: async (): Promise<BanHealthDetailedResponse> => {
const response = await apiClient.get('/cabinet/admin/ban-system/health/detailed')
return response.data
const response = await apiClient.get('/cabinet/admin/ban-system/health/detailed');
return response.data;
},
// Agent History
getAgentHistory: async (nodeName: string, hours: number = 24): Promise<BanAgentHistoryResponse> => {
const response = await apiClient.get(`/cabinet/admin/ban-system/agents/${encodeURIComponent(nodeName)}/history`, {
params: { hours }
})
return response.data
getAgentHistory: async (
nodeName: string,
hours: number = 24,
): Promise<BanAgentHistoryResponse> => {
const response = await apiClient.get(
`/cabinet/admin/ban-system/agents/${encodeURIComponent(nodeName)}/history`,
{
params: { hours },
},
);
return response.data;
},
// User Punishment History
getUserHistory: async (email: string, limit: number = 20): Promise<BanHistoryResponse> => {
const response = await apiClient.get(`/cabinet/admin/ban-system/users/${encodeURIComponent(email)}/history`, {
params: { limit }
})
return response.data
const response = await apiClient.get(
`/cabinet/admin/ban-system/users/${encodeURIComponent(email)}/history`,
{
params: { limit },
},
);
return response.data;
},
}
};

View File

@@ -1,203 +1,209 @@
import apiClient from './client'
import apiClient from './client';
export interface BrandingInfo {
name: string
logo_url: string | null
logo_letter: string
has_custom_logo: boolean
name: string;
logo_url: string | null;
logo_letter: string;
has_custom_logo: boolean;
}
export interface AnimationEnabled {
enabled: boolean
enabled: boolean;
}
export interface FullscreenEnabled {
enabled: boolean
enabled: boolean;
}
export interface EmailAuthEnabled {
enabled: boolean
enabled: boolean;
}
export interface AnalyticsCounters {
yandex_metrika_id: string
google_ads_id: string
google_ads_label: string
yandex_metrika_id: string;
google_ads_id: string;
google_ads_label: string;
}
const BRANDING_CACHE_KEY = 'cabinet_branding'
const LOGO_PRELOADED_KEY = 'cabinet_logo_preloaded'
const BRANDING_CACHE_KEY = 'cabinet_branding';
const LOGO_PRELOADED_KEY = 'cabinet_logo_preloaded';
// Check if logo was already preloaded in this session
export const isLogoPreloaded = (): boolean => {
try {
const cached = getCachedBranding()
const cached = getCachedBranding();
if (!cached?.has_custom_logo || !cached?.logo_url) {
return false
return false;
}
const logoUrl = `${import.meta.env.VITE_API_URL || ''}${cached.logo_url}`
const preloaded = sessionStorage.getItem(LOGO_PRELOADED_KEY)
return preloaded === logoUrl
const logoUrl = `${import.meta.env.VITE_API_URL || ''}${cached.logo_url}`;
const preloaded = sessionStorage.getItem(LOGO_PRELOADED_KEY);
return preloaded === logoUrl;
} catch {
return false
return false;
}
}
};
// Get cached branding from localStorage
export const getCachedBranding = (): BrandingInfo | null => {
try {
const cached = localStorage.getItem(BRANDING_CACHE_KEY)
const cached = localStorage.getItem(BRANDING_CACHE_KEY);
if (cached) {
return JSON.parse(cached)
return JSON.parse(cached);
}
} catch {
// localStorage not available or invalid JSON
}
return null
}
return null;
};
// Update branding cache in localStorage
export const setCachedBranding = (branding: BrandingInfo) => {
try {
localStorage.setItem(BRANDING_CACHE_KEY, JSON.stringify(branding))
localStorage.setItem(BRANDING_CACHE_KEY, JSON.stringify(branding));
} catch {
// localStorage not available
}
}
};
// Preload logo image for instant display
export const preloadLogo = (branding: BrandingInfo): Promise<void> => {
return new Promise((resolve) => {
if (!branding.has_custom_logo || !branding.logo_url) {
resolve()
return
resolve();
return;
}
const logoUrl = `${import.meta.env.VITE_API_URL || ''}${branding.logo_url}`
const logoUrl = `${import.meta.env.VITE_API_URL || ''}${branding.logo_url}`;
// Check if already preloaded in this session
const preloaded = sessionStorage.getItem(LOGO_PRELOADED_KEY)
const preloaded = sessionStorage.getItem(LOGO_PRELOADED_KEY);
if (preloaded === logoUrl) {
resolve()
return
resolve();
return;
}
const img = new Image()
const img = new Image();
img.onload = () => {
sessionStorage.setItem(LOGO_PRELOADED_KEY, logoUrl)
resolve()
}
img.onerror = () => resolve()
img.src = logoUrl
})
}
sessionStorage.setItem(LOGO_PRELOADED_KEY, logoUrl);
resolve();
};
img.onerror = () => resolve();
img.src = logoUrl;
});
};
// Initialize logo preload from cache on page load
export const initLogoPreload = () => {
const cached = getCachedBranding()
const cached = getCachedBranding();
if (cached) {
preloadLogo(cached)
preloadLogo(cached);
}
}
};
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
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
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 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
});
return response.data;
},
// Delete custom logo (admin only)
deleteLogo: async (): Promise<BrandingInfo> => {
const response = await apiClient.delete<BrandingInfo>('/cabinet/branding/logo')
return response.data
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 null;
}
return `${import.meta.env.VITE_API_URL || ''}${branding.logo_url}`
return `${import.meta.env.VITE_API_URL || ''}${branding.logo_url}`;
},
// Get animation enabled (public, no auth required)
getAnimationEnabled: async (): Promise<AnimationEnabled> => {
const response = await apiClient.get<AnimationEnabled>('/cabinet/branding/animation')
return response.data
const response = await apiClient.get<AnimationEnabled>('/cabinet/branding/animation');
return response.data;
},
// Update animation enabled (admin only)
updateAnimationEnabled: async (enabled: boolean): Promise<AnimationEnabled> => {
const response = await apiClient.patch<AnimationEnabled>('/cabinet/branding/animation', { enabled })
return response.data
const response = await apiClient.patch<AnimationEnabled>('/cabinet/branding/animation', {
enabled,
});
return response.data;
},
// Get fullscreen enabled (public, no auth required)
getFullscreenEnabled: async (): Promise<FullscreenEnabled> => {
try {
const response = await apiClient.get<FullscreenEnabled>('/cabinet/branding/fullscreen')
return response.data
const response = await apiClient.get<FullscreenEnabled>('/cabinet/branding/fullscreen');
return response.data;
} catch {
// If endpoint doesn't exist, default to disabled
return { enabled: false }
return { enabled: false };
}
},
// Update fullscreen enabled (admin only)
updateFullscreenEnabled: async (enabled: boolean): Promise<FullscreenEnabled> => {
const response = await apiClient.patch<FullscreenEnabled>('/cabinet/branding/fullscreen', { enabled })
return response.data
const response = await apiClient.patch<FullscreenEnabled>('/cabinet/branding/fullscreen', {
enabled,
});
return response.data;
},
// Get email auth enabled (public, no auth required)
getEmailAuthEnabled: async (): Promise<EmailAuthEnabled> => {
try {
const response = await apiClient.get<EmailAuthEnabled>('/cabinet/branding/email-auth')
return response.data
const response = await apiClient.get<EmailAuthEnabled>('/cabinet/branding/email-auth');
return response.data;
} catch {
// If endpoint doesn't exist, default to enabled
return { enabled: true }
return { enabled: true };
}
},
// Update email auth enabled (admin only)
updateEmailAuthEnabled: async (enabled: boolean): Promise<EmailAuthEnabled> => {
const response = await apiClient.patch<EmailAuthEnabled>('/cabinet/branding/email-auth', { enabled })
return response.data
const response = await apiClient.patch<EmailAuthEnabled>('/cabinet/branding/email-auth', {
enabled,
});
return response.data;
},
// Get analytics counters (public, no auth required)
getAnalyticsCounters: async (): Promise<AnalyticsCounters> => {
try {
const response = await apiClient.get<AnalyticsCounters>('/cabinet/branding/analytics')
return response.data
const response = await apiClient.get<AnalyticsCounters>('/cabinet/branding/analytics');
return response.data;
} catch {
return { yandex_metrika_id: '', google_ads_id: '', google_ads_label: '' }
return { yandex_metrika_id: '', google_ads_id: '', google_ads_label: '' };
}
},
// Update analytics counters (admin only)
updateAnalyticsCounters: async (data: Partial<AnalyticsCounters>): Promise<AnalyticsCounters> => {
const response = await apiClient.patch<AnalyticsCounters>('/cabinet/branding/analytics', data)
return response.data
const response = await apiClient.patch<AnalyticsCounters>('/cabinet/branding/analytics', data);
return response.data;
},
}
};

View File

@@ -1,230 +1,241 @@
import apiClient from './client'
import apiClient from './client';
// Types
export type CampaignBonusType = 'balance' | 'subscription' | 'none' | 'tariff'
export type CampaignBonusType = 'balance' | 'subscription' | 'none' | 'tariff';
export interface TariffInfo {
id: number
name: string
id: number;
name: string;
}
export interface CampaignListItem {
id: number
name: string
start_parameter: string
bonus_type: CampaignBonusType
is_active: boolean
registrations_count: number
total_revenue_kopeks: number
conversion_rate: number
created_at: string
id: number;
name: string;
start_parameter: string;
bonus_type: CampaignBonusType;
is_active: boolean;
registrations_count: number;
total_revenue_kopeks: number;
conversion_rate: number;
created_at: string;
}
export interface CampaignListResponse {
campaigns: CampaignListItem[]
total: number
campaigns: CampaignListItem[];
total: number;
}
export interface CampaignDetail {
id: number
name: string
start_parameter: string
bonus_type: CampaignBonusType
is_active: boolean
balance_bonus_kopeks: number
balance_bonus_rubles: number
subscription_duration_days: number | null
subscription_traffic_gb: number | null
subscription_device_limit: number | null
subscription_squads: string[]
tariff_id: number | null
tariff_duration_days: number | null
tariff: TariffInfo | null
created_by: number | null
created_at: string
updated_at: string | null
deep_link: string | null
id: number;
name: string;
start_parameter: string;
bonus_type: CampaignBonusType;
is_active: boolean;
balance_bonus_kopeks: number;
balance_bonus_rubles: number;
subscription_duration_days: number | null;
subscription_traffic_gb: number | null;
subscription_device_limit: number | null;
subscription_squads: string[];
tariff_id: number | null;
tariff_duration_days: number | null;
tariff: TariffInfo | null;
created_by: number | null;
created_at: string;
updated_at: string | null;
deep_link: string | null;
}
export interface CampaignCreateRequest {
name: string
start_parameter: string
bonus_type: CampaignBonusType
is_active?: boolean
balance_bonus_kopeks?: number
subscription_duration_days?: number
subscription_traffic_gb?: number
subscription_device_limit?: number
subscription_squads?: string[]
tariff_id?: number
tariff_duration_days?: number
name: string;
start_parameter: string;
bonus_type: CampaignBonusType;
is_active?: boolean;
balance_bonus_kopeks?: number;
subscription_duration_days?: number;
subscription_traffic_gb?: number;
subscription_device_limit?: number;
subscription_squads?: string[];
tariff_id?: number;
tariff_duration_days?: number;
}
export interface CampaignUpdateRequest {
name?: string
start_parameter?: string
bonus_type?: CampaignBonusType
is_active?: boolean
balance_bonus_kopeks?: number
subscription_duration_days?: number
subscription_traffic_gb?: number
subscription_device_limit?: number
subscription_squads?: string[]
tariff_id?: number
tariff_duration_days?: number
name?: string;
start_parameter?: string;
bonus_type?: CampaignBonusType;
is_active?: boolean;
balance_bonus_kopeks?: number;
subscription_duration_days?: number;
subscription_traffic_gb?: number;
subscription_device_limit?: number;
subscription_squads?: string[];
tariff_id?: number;
tariff_duration_days?: number;
}
export interface CampaignToggleResponse {
id: number
is_active: boolean
message: string
id: number;
is_active: boolean;
message: string;
}
export interface CampaignStatistics {
id: number
name: string
start_parameter: string
bonus_type: CampaignBonusType
is_active: boolean
registrations: number
balance_issued_kopeks: number
balance_issued_rubles: number
subscription_issued: number
last_registration: string | null
total_revenue_kopeks: number
total_revenue_rubles: number
avg_revenue_per_user_kopeks: number
avg_revenue_per_user_rubles: number
avg_first_payment_kopeks: number
avg_first_payment_rubles: number
trial_users_count: number
active_trials_count: number
conversion_count: number
paid_users_count: number
conversion_rate: number
trial_conversion_rate: number
deep_link: string | null
id: number;
name: string;
start_parameter: string;
bonus_type: CampaignBonusType;
is_active: boolean;
registrations: number;
balance_issued_kopeks: number;
balance_issued_rubles: number;
subscription_issued: number;
last_registration: string | null;
total_revenue_kopeks: number;
total_revenue_rubles: number;
avg_revenue_per_user_kopeks: number;
avg_revenue_per_user_rubles: number;
avg_first_payment_kopeks: number;
avg_first_payment_rubles: number;
trial_users_count: number;
active_trials_count: number;
conversion_count: number;
paid_users_count: number;
conversion_rate: number;
trial_conversion_rate: number;
deep_link: string | null;
}
export interface CampaignRegistrationItem {
id: number
user_id: number
telegram_id: number
username: string | null
first_name: string | null
bonus_type: string
balance_bonus_kopeks: number
subscription_duration_days: number | null
tariff_id: number | null
tariff_duration_days: number | null
created_at: string
user_balance_kopeks: number
has_subscription: boolean
has_paid: boolean
id: number;
user_id: number;
telegram_id: number;
username: string | null;
first_name: string | null;
bonus_type: string;
balance_bonus_kopeks: number;
subscription_duration_days: number | null;
tariff_id: number | null;
tariff_duration_days: number | null;
created_at: string;
user_balance_kopeks: number;
has_subscription: boolean;
has_paid: boolean;
}
export interface CampaignRegistrationsResponse {
registrations: CampaignRegistrationItem[]
total: number
page: number
per_page: number
registrations: CampaignRegistrationItem[];
total: number;
page: number;
per_page: number;
}
export interface CampaignsOverview {
total: number
active: number
inactive: number
total_registrations: number
total_balance_issued_kopeks: number
total_balance_issued_rubles: number
total_subscription_issued: number
total_tariff_issued: number
total: number;
active: number;
inactive: number;
total_registrations: number;
total_balance_issued_kopeks: number;
total_balance_issued_rubles: number;
total_subscription_issued: number;
total_tariff_issued: number;
}
export interface ServerSquadInfo {
id: number
squad_uuid: string
display_name: string
country_code: string | null
id: number;
squad_uuid: string;
display_name: string;
country_code: string | null;
}
export interface TariffListItem {
id: number
name: string
description: string | null
is_active: boolean
traffic_limit_gb: number
device_limit: number
id: number;
name: string;
description: string | null;
is_active: boolean;
traffic_limit_gb: number;
device_limit: number;
}
export const campaignsApi = {
// Get campaigns overview
getOverview: async (): Promise<CampaignsOverview> => {
const response = await apiClient.get('/cabinet/admin/campaigns/overview')
return response.data
const response = await apiClient.get('/cabinet/admin/campaigns/overview');
return response.data;
},
// Get all campaigns
getCampaigns: async (includeInactive = true, offset = 0, limit = 50): Promise<CampaignListResponse> => {
getCampaigns: async (
includeInactive = true,
offset = 0,
limit = 50,
): Promise<CampaignListResponse> => {
const response = await apiClient.get('/cabinet/admin/campaigns', {
params: { include_inactive: includeInactive, offset, limit }
})
return response.data
params: { include_inactive: includeInactive, offset, limit },
});
return response.data;
},
// Get single campaign
getCampaign: async (campaignId: number): Promise<CampaignDetail> => {
const response = await apiClient.get(`/cabinet/admin/campaigns/${campaignId}`)
return response.data
const response = await apiClient.get(`/cabinet/admin/campaigns/${campaignId}`);
return response.data;
},
// Get campaign statistics
getCampaignStats: async (campaignId: number): Promise<CampaignStatistics> => {
const response = await apiClient.get(`/cabinet/admin/campaigns/${campaignId}/stats`)
return response.data
const response = await apiClient.get(`/cabinet/admin/campaigns/${campaignId}/stats`);
return response.data;
},
// Get campaign registrations
getCampaignRegistrations: async (campaignId: number, page = 1, perPage = 50): Promise<CampaignRegistrationsResponse> => {
getCampaignRegistrations: async (
campaignId: number,
page = 1,
perPage = 50,
): Promise<CampaignRegistrationsResponse> => {
const response = await apiClient.get(`/cabinet/admin/campaigns/${campaignId}/registrations`, {
params: { page, per_page: perPage }
})
return response.data
params: { page, per_page: perPage },
});
return response.data;
},
// Create campaign
createCampaign: async (data: CampaignCreateRequest): Promise<CampaignDetail> => {
const response = await apiClient.post('/cabinet/admin/campaigns', data)
return response.data
const response = await apiClient.post('/cabinet/admin/campaigns', data);
return response.data;
},
// Update campaign
updateCampaign: async (campaignId: number, data: CampaignUpdateRequest): Promise<CampaignDetail> => {
const response = await apiClient.put(`/cabinet/admin/campaigns/${campaignId}`, data)
return response.data
updateCampaign: async (
campaignId: number,
data: CampaignUpdateRequest,
): Promise<CampaignDetail> => {
const response = await apiClient.put(`/cabinet/admin/campaigns/${campaignId}`, data);
return response.data;
},
// Delete campaign
deleteCampaign: async (campaignId: number): Promise<{ message: string }> => {
const response = await apiClient.delete(`/cabinet/admin/campaigns/${campaignId}`)
return response.data
const response = await apiClient.delete(`/cabinet/admin/campaigns/${campaignId}`);
return response.data;
},
// Toggle campaign active status
toggleCampaign: async (campaignId: number): Promise<CampaignToggleResponse> => {
const response = await apiClient.post(`/cabinet/admin/campaigns/${campaignId}/toggle`)
return response.data
const response = await apiClient.post(`/cabinet/admin/campaigns/${campaignId}/toggle`);
return response.data;
},
// Get available servers for subscription bonus
getAvailableServers: async (): Promise<ServerSquadInfo[]> => {
const response = await apiClient.get('/cabinet/admin/campaigns/available-servers')
return response.data
const response = await apiClient.get('/cabinet/admin/campaigns/available-servers');
return response.data;
},
// Get available tariffs for tariff bonus
getAvailableTariffs: async (): Promise<TariffListItem[]> => {
const response = await apiClient.get('/cabinet/admin/campaigns/available-tariffs')
return response.data
const response = await apiClient.get('/cabinet/admin/campaigns/available-tariffs');
return response.data;
},
}
};

View File

@@ -1,163 +1,175 @@
import axios, { AxiosError, InternalAxiosRequestConfig } from 'axios'
import { tokenStorage, isTokenExpired, tokenRefreshManager, safeRedirectToLogin } from '../utils/token'
import { useBlockingStore } from '../store/blocking'
import axios, { AxiosError, InternalAxiosRequestConfig } from 'axios';
import {
tokenStorage,
isTokenExpired,
tokenRefreshManager,
safeRedirectToLogin,
} from '../utils/token';
import { useBlockingStore } from '../store/blocking';
const API_BASE_URL = import.meta.env.VITE_API_URL || '/api'
const API_BASE_URL = import.meta.env.VITE_API_URL || '/api';
// Настраиваем endpoint для refresh
tokenRefreshManager.setRefreshEndpoint(`${API_BASE_URL}/cabinet/auth/refresh`)
tokenRefreshManager.setRefreshEndpoint(`${API_BASE_URL}/cabinet/auth/refresh`);
// CSRF token management
const CSRF_COOKIE_NAME = 'csrf_token'
const CSRF_HEADER_NAME = 'X-CSRF-Token'
const CSRF_COOKIE_NAME = 'csrf_token';
const CSRF_HEADER_NAME = 'X-CSRF-Token';
function getCsrfToken(): string | null {
if (typeof document === 'undefined') return null
const match = document.cookie.match(new RegExp(`(^| )${CSRF_COOKIE_NAME}=([^;]+)`))
return match ? match[2] : null
if (typeof document === 'undefined') return null;
const match = document.cookie.match(new RegExp(`(^| )${CSRF_COOKIE_NAME}=([^;]+)`));
return match ? match[2] : null;
}
function generateCsrfToken(): string {
const array = new Uint8Array(32)
crypto.getRandomValues(array)
return Array.from(array, byte => byte.toString(16).padStart(2, '0')).join('')
const array = new Uint8Array(32);
crypto.getRandomValues(array);
return Array.from(array, (byte) => byte.toString(16).padStart(2, '0')).join('');
}
function ensureCsrfToken(): string {
let token = getCsrfToken()
let token = getCsrfToken();
if (!token) {
token = generateCsrfToken()
token = generateCsrfToken();
// Set cookie with SameSite=Strict for CSRF protection
document.cookie = `${CSRF_COOKIE_NAME}=${token}; path=/; SameSite=Strict; Secure`
document.cookie = `${CSRF_COOKIE_NAME}=${token}; path=/; SameSite=Strict; Secure`;
}
return token
return token;
}
const getTelegramInitData = (): string | null => {
if (typeof window === 'undefined') return null
if (typeof window === 'undefined') return null;
const initData = window.Telegram?.WebApp?.initData
const initData = window.Telegram?.WebApp?.initData;
if (initData) {
tokenStorage.setTelegramInitData(initData)
return initData
tokenStorage.setTelegramInitData(initData);
return initData;
}
return tokenStorage.getTelegramInitData()
}
return tokenStorage.getTelegramInitData();
};
export const apiClient = axios.create({
baseURL: API_BASE_URL,
headers: {
'Content-Type': 'application/json',
},
})
});
// Request interceptor - add auth token with expiration check
apiClient.interceptors.request.use(async (config: InternalAxiosRequestConfig) => {
let token = tokenStorage.getAccessToken()
let token = tokenStorage.getAccessToken();
// Проверяем срок действия токена перед запросом
if (token && isTokenExpired(token)) {
// Используем централизованный менеджер для refresh
const newToken = await tokenRefreshManager.refreshAccessToken()
const newToken = await tokenRefreshManager.refreshAccessToken();
if (newToken) {
token = newToken
token = newToken;
} else {
// Refresh не удался - редирект на логин
tokenStorage.clearTokens()
safeRedirectToLogin()
return config
tokenStorage.clearTokens();
safeRedirectToLogin();
return config;
}
}
if (token && config.headers) {
config.headers.Authorization = `Bearer ${token}`
config.headers.Authorization = `Bearer ${token}`;
}
const telegramInitData = getTelegramInitData()
const telegramInitData = getTelegramInitData();
if (telegramInitData && config.headers) {
config.headers['X-Telegram-Init-Data'] = telegramInitData
config.headers['X-Telegram-Init-Data'] = telegramInitData;
}
// Add CSRF token for state-changing methods
const method = config.method?.toUpperCase()
const method = config.method?.toUpperCase();
if (method && ['POST', 'PUT', 'DELETE', 'PATCH'].includes(method) && config.headers) {
config.headers[CSRF_HEADER_NAME] = ensureCsrfToken()
config.headers[CSRF_HEADER_NAME] = ensureCsrfToken();
}
return config
})
return config;
});
// Custom error types for special handling
export interface MaintenanceError {
code: 'maintenance'
message: string
reason?: string
code: 'maintenance';
message: string;
reason?: string;
}
export interface ChannelSubscriptionError {
code: 'channel_subscription_required'
message: string
channel_link?: string
code: 'channel_subscription_required';
message: string;
channel_link?: string;
}
export function isMaintenanceError(error: unknown): error is { response: { status: 503, data: { detail: MaintenanceError } } } {
if (!error || typeof error !== 'object') return false
const err = error as AxiosError<{ detail: MaintenanceError }>
return err.response?.status === 503 && err.response?.data?.detail?.code === 'maintenance'
export function isMaintenanceError(
error: unknown,
): error is { response: { status: 503; data: { detail: MaintenanceError } } } {
if (!error || typeof error !== 'object') return false;
const err = error as AxiosError<{ detail: MaintenanceError }>;
return err.response?.status === 503 && err.response?.data?.detail?.code === 'maintenance';
}
export function isChannelSubscriptionError(error: unknown): error is { response: { status: 403, data: { detail: ChannelSubscriptionError } } } {
if (!error || typeof error !== 'object') return false
const err = error as AxiosError<{ detail: ChannelSubscriptionError }>
return err.response?.status === 403 && err.response?.data?.detail?.code === 'channel_subscription_required'
export function isChannelSubscriptionError(
error: unknown,
): error is { response: { status: 403; data: { detail: ChannelSubscriptionError } } } {
if (!error || typeof error !== 'object') return false;
const err = error as AxiosError<{ detail: ChannelSubscriptionError }>;
return (
err.response?.status === 403 &&
err.response?.data?.detail?.code === 'channel_subscription_required'
);
}
// Response interceptor - handle 401, 503 (maintenance), 403 (channel subscription)
apiClient.interceptors.response.use(
(response) => response,
async (error: AxiosError) => {
const originalRequest = error.config as InternalAxiosRequestConfig & { _retry?: boolean }
const originalRequest = error.config as InternalAxiosRequestConfig & { _retry?: boolean };
// Handle maintenance mode (503)
if (isMaintenanceError(error)) {
const detail = (error.response?.data as { detail: MaintenanceError }).detail
const detail = (error.response?.data as { detail: MaintenanceError }).detail;
useBlockingStore.getState().setMaintenance({
message: detail.message,
reason: detail.reason,
})
return Promise.reject(error)
});
return Promise.reject(error);
}
// Handle channel subscription required (403)
if (isChannelSubscriptionError(error)) {
const detail = (error.response?.data as { detail: ChannelSubscriptionError }).detail
const detail = (error.response?.data as { detail: ChannelSubscriptionError }).detail;
useBlockingStore.getState().setChannelSubscription({
message: detail.message,
channel_link: detail.channel_link,
})
return Promise.reject(error)
});
return Promise.reject(error);
}
// Если получили 401 и ещё не пробовали refresh (на случай если проверка exp не сработала)
if (error.response?.status === 401 && !originalRequest._retry) {
originalRequest._retry = true
originalRequest._retry = true;
const newToken = await tokenRefreshManager.refreshAccessToken()
const newToken = await tokenRefreshManager.refreshAccessToken();
if (newToken) {
if (originalRequest.headers) {
originalRequest.headers.Authorization = `Bearer ${newToken}`
originalRequest.headers.Authorization = `Bearer ${newToken}`;
}
return apiClient(originalRequest)
return apiClient(originalRequest);
} else {
// Refresh не удался
tokenStorage.clearTokens()
safeRedirectToLogin()
tokenStorage.clearTokens();
safeRedirectToLogin();
}
}
return Promise.reject(error)
}
)
return Promise.reject(error);
},
);
export default apiClient
export default apiClient;

View File

@@ -1,49 +1,50 @@
import apiClient from './client'
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
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
round_id: number;
game_type: string;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
game_data: Record<string, any>;
instructions: string;
}
export interface ContestResult {
is_winner: boolean
message: string
prize_days?: number
is_winner: boolean;
message: string;
prize_days?: number;
}
export interface ContestsCountResponse {
count: number
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
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
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
const response = await apiClient.get<ContestGameData>(`/cabinet/contests/${roundId}`);
return response.data;
},
// Submit answer for a contest
@@ -51,7 +52,7 @@ export const contestsApi = {
const response = await apiClient.post<ContestResult>(`/cabinet/contests/${roundId}/answer`, {
round_id: roundId,
answer,
})
return response.data
});
return response.data;
},
}
};

View File

@@ -1,46 +1,46 @@
// Currency exchange rate API
// Uses free exchangerate.host API
import axios from 'axios'
import axios from 'axios';
interface ExchangeRates {
USD: number
CNY: number
IRR: number
USD: number;
CNY: number;
IRR: number;
}
interface CachedRates {
rates: ExchangeRates
timestamp: number
rates: ExchangeRates;
timestamp: number;
}
const CACHE_DURATION = 60 * 60 * 1000 // 1 hour in milliseconds
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)
}
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
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
return cachedRates.rates;
}
try {
// Try primary API (exchangerate.host) - get RUB based rates
const response = await axios.get<{
success?: boolean
rates?: { USD?: number; CNY?: number; IRR?: number }
success?: boolean;
rates?: { USD?: number; CNY?: number; IRR?: number };
}>('https://api.exchangerate.host/latest', {
params: { base: 'RUB', symbols: 'USD,CNY,IRR' },
})
});
if (response.data.success && response.data.rates) {
// API returns how much of each currency equals 1 RUB
@@ -49,51 +49,65 @@ export const currencyApi = {
USD: response.data.rates.USD ? 1 / response.data.rates.USD : FALLBACK_RATES.USD,
CNY: response.data.rates.CNY ? 1 / response.data.rates.CNY : FALLBACK_RATES.CNY,
IRR: response.data.rates.IRR ? 1 / response.data.rates.IRR : FALLBACK_RATES.IRR,
}
cachedRates = { rates, timestamp: Date.now() }
return rates
};
cachedRates = { rates, timestamp: Date.now() };
return rates;
}
// Try backup API (open.er-api.com)
const backupResponse = await axios.get<{
rates?: { USD?: number; CNY?: number; IRR?: number }
}>('https://open.er-api.com/v6/latest/RUB')
rates?: { USD?: number; CNY?: number; IRR?: number };
}>('https://open.er-api.com/v6/latest/RUB');
if (backupResponse.data.rates) {
const rates: ExchangeRates = {
USD: backupResponse.data.rates.USD ? 1 / backupResponse.data.rates.USD : FALLBACK_RATES.USD,
CNY: backupResponse.data.rates.CNY ? 1 / backupResponse.data.rates.CNY : FALLBACK_RATES.CNY,
IRR: backupResponse.data.rates.IRR ? 1 / backupResponse.data.rates.IRR : FALLBACK_RATES.IRR,
}
cachedRates = { rates, timestamp: Date.now() }
return rates
USD: backupResponse.data.rates.USD
? 1 / backupResponse.data.rates.USD
: FALLBACK_RATES.USD,
CNY: backupResponse.data.rates.CNY
? 1 / backupResponse.data.rates.CNY
: FALLBACK_RATES.CNY,
IRR: backupResponse.data.rates.IRR
? 1 / backupResponse.data.rates.IRR
: FALLBACK_RATES.IRR,
};
cachedRates = { rates, timestamp: Date.now() };
return rates;
}
// Return fallback rates if both APIs fail
return FALLBACK_RATES
return FALLBACK_RATES;
} catch (error) {
console.warn('Failed to fetch exchange rates, using fallback:', error)
return FALLBACK_RATES
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]
convertFromRub: (
rubAmount: number,
targetCurrency: keyof ExchangeRates,
rates: ExchangeRates,
): number => {
const rate = rates[targetCurrency];
if (!rate || rate <= 0) {
return rubAmount / FALLBACK_RATES[targetCurrency]
return rubAmount / FALLBACK_RATES[targetCurrency];
}
return rubAmount / rate
return rubAmount / rate;
},
// Convert from target currency to RUB
convertToRub: (amount: number, sourceCurrency: keyof ExchangeRates, rates: ExchangeRates): number => {
const rate = rates[sourceCurrency]
convertToRub: (
amount: number,
sourceCurrency: keyof ExchangeRates,
rates: ExchangeRates,
): number => {
const rate = rates[sourceCurrency];
if (!rate || rate <= 0) {
return amount * FALLBACK_RATES[sourceCurrency]
return amount * FALLBACK_RATES[sourceCurrency];
}
return amount * rate
return amount * rate;
},
}
};
export type { ExchangeRates }
export type { ExchangeRates };

View File

@@ -1,12 +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'
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';

View File

@@ -1,102 +1,102 @@
import apiClient from './client'
import type { SupportConfig } from '../types'
import apiClient from './client';
import type { SupportConfig } from '../types';
export interface FaqPage {
id: number
title: string
content: string
order: number
id: number;
title: string;
content: string;
order: number;
}
export interface RulesResponse {
content: string
updated_at: string | null
content: string;
updated_at: string | null;
}
export interface PrivacyPolicyResponse {
content: string
updated_at: string | null
content: string;
updated_at: string | null;
}
export interface PublicOfferResponse {
content: string
updated_at: string | null
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
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
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
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
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
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
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
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
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
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
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
});
return response.data;
},
// Get support configuration
getSupportConfig: async (): Promise<SupportConfig> => {
const response = await apiClient.get<SupportConfig>('/cabinet/info/support-config')
return response.data
const response = await apiClient.get<SupportConfig>('/cabinet/info/support-config');
return response.data;
},
}
};

View File

@@ -1,22 +1,22 @@
import axios, { AxiosError } from 'axios'
import axios, { AxiosError } from 'axios';
export interface MiniappCreatePaymentPayload {
initData: string
method: string
amountKopeks?: number | null
option?: string | null
initData: string;
method: string;
amountKopeks?: number | null;
option?: string | null;
}
export interface MiniappCreatePaymentResponse {
payment_url: string
amount_kopeks?: number
extra?: Record<string, unknown>
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
payload: MiniappCreatePaymentPayload,
): Promise<MiniappCreatePaymentResponse> => {
try {
const response = await axios.post<MiniappCreatePaymentResponse>(
@@ -29,15 +29,16 @@ export const miniappApi = {
},
{
headers: { 'Content-Type': 'application/json' },
}
)
return response.data
},
);
return response.data;
} catch (error) {
const axiosError = error as AxiosError<{ detail?: string; message?: string }>
const message = axiosError.response?.data?.detail
|| axiosError.response?.data?.message
|| 'Failed to create payment'
throw new Error(message)
const axiosError = error as AxiosError<{ detail?: string; message?: string }>;
const message =
axiosError.response?.data?.detail ||
axiosError.response?.data?.message ||
'Failed to create payment';
throw new Error(message);
}
},
}
};

View File

@@ -1,58 +1,64 @@
import apiClient from './client'
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
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
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
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
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
'/cabinet/notifications/test',
);
return response.data;
},
// Get notification history
getHistory: async (limit = 20, offset = 0): Promise<{
notifications: any[]
total: number
limit: number
offset: number
getHistory: async (
limit = 20,
offset = 0,
): Promise<{
notifications: unknown[];
total: number;
limit: number;
offset: number;
}> => {
const response = await apiClient.get('/cabinet/notifications/history', {
params: { limit, offset },
})
return response.data
});
return response.data;
},
}
};

View File

@@ -1,85 +1,85 @@
import apiClient from './client'
import apiClient from './client';
export interface PollOption {
id: number
text: string
order: number
id: number;
text: string;
order: number;
}
export interface PollQuestion {
id: number
text: string
order: number
options: PollOption[]
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
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
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
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
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
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
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
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
const response = await apiClient.post<PollStartResponse>(`/cabinet/polls/${responseId}/start`);
return response.data;
},
// Answer a question
answerQuestion: async (
responseId: number,
questionId: number,
optionId: number
optionId: number,
): Promise<PollAnswerResponse> => {
const response = await apiClient.post<PollAnswerResponse>(
`/cabinet/polls/${responseId}/questions/${questionId}/answer`,
{ option_id: optionId }
)
return response.data
{ option_id: optionId },
);
return response.data;
},
}
};

View File

@@ -1,96 +1,97 @@
import apiClient from './client'
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
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;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
extra_data: Record<string, any> | null;
}
export interface ActiveDiscount {
discount_percent: number
source: string | null
expires_at: string | null
is_active: boolean
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>
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
success: boolean;
message: string;
discount_percent: number | null;
expires_at: string | null;
}
export interface LoyaltyTierInfo {
id: number
name: string
threshold_rubles: number
server_discount_percent: number
traffic_discount_percent: number
device_discount_percent: number
period_discounts: Record<string, number>
is_current: boolean
is_achieved: boolean
id: number;
name: string;
threshold_rubles: number;
server_discount_percent: number;
traffic_discount_percent: number;
device_discount_percent: number;
period_discounts: Record<string, number>;
is_current: boolean;
is_achieved: boolean;
}
export interface LoyaltyTiersResponse {
tiers: LoyaltyTierInfo[]
current_spent_rubles: number
current_tier_name: string | null
next_tier_name: string | null
next_tier_threshold_rubles: number | null
progress_percent: number
tiers: LoyaltyTierInfo[];
current_spent_rubles: number;
current_tier_name: string | null;
next_tier_name: string | null;
next_tier_threshold_rubles: number | null;
progress_percent: number;
}
export const promoApi = {
// Get available promo offers
getOffers: async (): Promise<PromoOffer[]> => {
const response = await apiClient.get<PromoOffer[]>('/cabinet/promo/offers')
return response.data
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
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
const response = await apiClient.get<PromoGroupDiscounts>('/cabinet/promo/group-discounts');
return response.data;
},
// Get loyalty tiers (promo groups with spending thresholds)
getLoyaltyTiers: async (): Promise<LoyaltyTiersResponse> => {
const response = await apiClient.get<LoyaltyTiersResponse>('/cabinet/promo/loyalty-tiers')
return response.data
const response = await apiClient.get<LoyaltyTiersResponse>('/cabinet/promo/loyalty-tiers');
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
});
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
const response = await apiClient.delete<{ message: string }>('/cabinet/promo/active-discount');
return response.data;
},
}
};

View File

@@ -1,139 +1,140 @@
import apiClient from './client'
import apiClient from './client';
// ============== Types ==============
export interface PromoOfferUserInfo {
id: number
telegram_id: number
username: string | null
first_name: string | null
last_name: string | null
full_name: string | null
id: number;
telegram_id: number;
username: string | null;
first_name: string | null;
last_name: string | null;
full_name: string | null;
}
export interface PromoOfferSubscriptionInfo {
id: number
status: string
is_trial: boolean
start_date: string
end_date: string
autopay_enabled: boolean
id: number;
status: string;
is_trial: boolean;
start_date: string;
end_date: string;
autopay_enabled: boolean;
}
export interface PromoOffer {
id: number
user_id: number
subscription_id: number | null
notification_type: string
discount_percent: number
bonus_amount_kopeks: number
expires_at: string
claimed_at: string | null
is_active: boolean
effect_type: string
extra_data: Record<string, any>
created_at: string
updated_at: string
user: PromoOfferUserInfo | null
subscription: PromoOfferSubscriptionInfo | null
id: number;
user_id: number;
subscription_id: number | null;
notification_type: string;
discount_percent: number;
bonus_amount_kopeks: number;
expires_at: string;
claimed_at: string | null;
is_active: boolean;
effect_type: string;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
extra_data: Record<string, any>;
created_at: string;
updated_at: string;
user: PromoOfferUserInfo | null;
subscription: PromoOfferSubscriptionInfo | null;
}
export interface PromoOfferListResponse {
items: PromoOffer[]
total: number
limit: number
offset: number
items: PromoOffer[];
total: number;
limit: number;
offset: number;
}
export interface PromoOfferBroadcastRequest {
notification_type: string
valid_hours: number
discount_percent?: number
bonus_amount_kopeks?: number
effect_type?: string
extra_data?: Record<string, any>
target?: string
user_id?: number
telegram_id?: number
notification_type: string;
valid_hours: number;
discount_percent?: number;
bonus_amount_kopeks?: number;
effect_type?: string;
extra_data?: Record<string, unknown>;
target?: string;
user_id?: number;
telegram_id?: number;
// Telegram notification options
send_notification?: boolean
message_text?: string
button_text?: string
send_notification?: boolean;
message_text?: string;
button_text?: string;
}
export interface PromoOfferBroadcastResponse {
created_offers: number
user_ids: number[]
target: string | null
notifications_sent: number
notifications_failed: number
created_offers: number;
user_ids: number[];
target: string | null;
notifications_sent: number;
notifications_failed: number;
}
export interface PromoOfferTemplate {
id: number
name: string
offer_type: string
message_text: string
button_text: string
valid_hours: number
discount_percent: number
bonus_amount_kopeks: number
active_discount_hours: number | null
test_duration_hours: number | null
test_squad_uuids: string[]
is_active: boolean
created_by: number | null
created_at: string
updated_at: string
id: number;
name: string;
offer_type: string;
message_text: string;
button_text: string;
valid_hours: number;
discount_percent: number;
bonus_amount_kopeks: number;
active_discount_hours: number | null;
test_duration_hours: number | null;
test_squad_uuids: string[];
is_active: boolean;
created_by: number | null;
created_at: string;
updated_at: string;
}
export interface PromoOfferTemplateListResponse {
items: PromoOfferTemplate[]
items: PromoOfferTemplate[];
}
export interface PromoOfferTemplateUpdateRequest {
name?: string
message_text?: string
button_text?: string
valid_hours?: number
discount_percent?: number
bonus_amount_kopeks?: number
active_discount_hours?: number
test_duration_hours?: number
test_squad_uuids?: string[]
is_active?: boolean
name?: string;
message_text?: string;
button_text?: string;
valid_hours?: number;
discount_percent?: number;
bonus_amount_kopeks?: number;
active_discount_hours?: number;
test_duration_hours?: number;
test_squad_uuids?: string[];
is_active?: boolean;
}
export interface PromoOfferLogOfferInfo {
id: number
notification_type: string | null
discount_percent: number | null
bonus_amount_kopeks: number | null
effect_type: string | null
expires_at: string | null
claimed_at: string | null
is_active: boolean | null
id: number;
notification_type: string | null;
discount_percent: number | null;
bonus_amount_kopeks: number | null;
effect_type: string | null;
expires_at: string | null;
claimed_at: string | null;
is_active: boolean | null;
}
export interface PromoOfferLog {
id: number
user_id: number | null
offer_id: number | null
action: string
source: string | null
percent: number | null
effect_type: string | null
details: Record<string, any>
created_at: string
user: PromoOfferUserInfo | null
offer: PromoOfferLogOfferInfo | null
id: number;
user_id: number | null;
offer_id: number | null;
action: string;
source: string | null;
percent: number | null;
effect_type: string | null;
details: Record<string, unknown>;
created_at: string;
user: PromoOfferUserInfo | null;
offer: PromoOfferLogOfferInfo | null;
}
export interface PromoOfferLogListResponse {
items: PromoOfferLog[]
total: number
limit: number
offset: number
items: PromoOfferLog[];
total: number;
limit: number;
offset: number;
}
// Target segments for broadcast
@@ -154,9 +155,9 @@ export const TARGET_SEGMENTS = {
custom_week: 'Зарегистрированы за неделю',
custom_month: 'Зарегистрированы за месяц',
custom_active_today: 'Активны сегодня',
} as const
} as const;
export type TargetSegment = keyof typeof TARGET_SEGMENTS
export type TargetSegment = keyof typeof TARGET_SEGMENTS;
// Offer type configurations
export const OFFER_TYPE_CONFIG = {
@@ -178,56 +179,61 @@ export const OFFER_TYPE_CONFIG = {
effect: 'percent_discount',
description: 'Скидка для новых пользователей',
},
} as const
} as const;
export type OfferType = keyof typeof OFFER_TYPE_CONFIG
export type OfferType = keyof typeof OFFER_TYPE_CONFIG;
// ============== API ==============
export const promoOffersApi = {
// Get list of promo offers
getOffers: async (params?: {
limit?: number
offset?: number
user_id?: number
is_active?: boolean
limit?: number;
offset?: number;
user_id?: number;
is_active?: boolean;
}): Promise<PromoOfferListResponse> => {
const response = await apiClient.get('/cabinet/admin/promo-offers', { params })
return response.data
const response = await apiClient.get('/cabinet/admin/promo-offers', { params });
return response.data;
},
// Broadcast offer to multiple users
broadcastOffer: async (data: PromoOfferBroadcastRequest): Promise<PromoOfferBroadcastResponse> => {
const response = await apiClient.post('/cabinet/admin/promo-offers/broadcast', data)
return response.data
broadcastOffer: async (
data: PromoOfferBroadcastRequest,
): Promise<PromoOfferBroadcastResponse> => {
const response = await apiClient.post('/cabinet/admin/promo-offers/broadcast', data);
return response.data;
},
// Get promo offer logs
getLogs: async (params?: {
limit?: number
offset?: number
user_id?: number
action?: string
limit?: number;
offset?: number;
user_id?: number;
action?: string;
}): Promise<PromoOfferLogListResponse> => {
const response = await apiClient.get('/cabinet/admin/promo-offers/logs', { params })
return response.data
const response = await apiClient.get('/cabinet/admin/promo-offers/logs', { params });
return response.data;
},
// Get all templates
getTemplates: async (): Promise<PromoOfferTemplateListResponse> => {
const response = await apiClient.get('/cabinet/admin/promo-offers/templates')
return response.data
const response = await apiClient.get('/cabinet/admin/promo-offers/templates');
return response.data;
},
// Get single template
getTemplate: async (id: number): Promise<PromoOfferTemplate> => {
const response = await apiClient.get(`/cabinet/admin/promo-offers/templates/${id}`)
return response.data
const response = await apiClient.get(`/cabinet/admin/promo-offers/templates/${id}`);
return response.data;
},
// Update template
updateTemplate: async (id: number, data: PromoOfferTemplateUpdateRequest): Promise<PromoOfferTemplate> => {
const response = await apiClient.patch(`/cabinet/admin/promo-offers/templates/${id}`, data)
return response.data
updateTemplate: async (
id: number,
data: PromoOfferTemplateUpdateRequest,
): Promise<PromoOfferTemplate> => {
const response = await apiClient.patch(`/cabinet/admin/promo-offers/templates/${id}`, data);
return response.data;
},
}
};

View File

@@ -1,122 +1,127 @@
import apiClient from './client'
import apiClient from './client';
// ============== Types ==============
export type PromoCodeType = 'balance' | 'subscription_days' | 'trial_subscription' | 'promo_group' | 'discount'
export type PromoCodeType =
| 'balance'
| 'subscription_days'
| 'trial_subscription'
| 'promo_group'
| 'discount';
export interface PromoCode {
id: number
code: string
type: PromoCodeType
balance_bonus_kopeks: number
balance_bonus_rubles: number
subscription_days: number
max_uses: number
current_uses: number
uses_left: number
is_active: boolean
is_valid: boolean
first_purchase_only: boolean
valid_from: string
valid_until: string | null
promo_group_id: number | null
created_by: number | null
created_at: string
updated_at: string
id: number;
code: string;
type: PromoCodeType;
balance_bonus_kopeks: number;
balance_bonus_rubles: number;
subscription_days: number;
max_uses: number;
current_uses: number;
uses_left: number;
is_active: boolean;
is_valid: boolean;
first_purchase_only: boolean;
valid_from: string;
valid_until: string | null;
promo_group_id: number | null;
created_by: number | null;
created_at: string;
updated_at: string;
}
export interface PromoCodeRecentUse {
id: number
user_id: number
user_username: string | null
user_full_name: string | null
user_telegram_id: number | null
used_at: string
id: number;
user_id: number;
user_username: string | null;
user_full_name: string | null;
user_telegram_id: number | null;
used_at: string;
}
export interface PromoCodeDetail extends PromoCode {
total_uses: number
today_uses: number
recent_uses: PromoCodeRecentUse[]
total_uses: number;
today_uses: number;
recent_uses: PromoCodeRecentUse[];
}
export interface PromoCodeListResponse {
items: PromoCode[]
total: number
limit: number
offset: number
items: PromoCode[];
total: number;
limit: number;
offset: number;
}
export interface PromoCodeCreateRequest {
code: string
type: PromoCodeType
balance_bonus_kopeks?: number
subscription_days?: number
max_uses?: number
valid_from?: string
valid_until?: string | null
is_active?: boolean
first_purchase_only?: boolean
promo_group_id?: number | null
code: string;
type: PromoCodeType;
balance_bonus_kopeks?: number;
subscription_days?: number;
max_uses?: number;
valid_from?: string;
valid_until?: string | null;
is_active?: boolean;
first_purchase_only?: boolean;
promo_group_id?: number | null;
}
export interface PromoCodeUpdateRequest {
code?: string
type?: PromoCodeType
balance_bonus_kopeks?: number
subscription_days?: number
max_uses?: number
valid_from?: string
valid_until?: string | null
is_active?: boolean
first_purchase_only?: boolean
promo_group_id?: number | null
code?: string;
type?: PromoCodeType;
balance_bonus_kopeks?: number;
subscription_days?: number;
max_uses?: number;
valid_from?: string;
valid_until?: string | null;
is_active?: boolean;
first_purchase_only?: boolean;
promo_group_id?: number | null;
}
// ============== PromoGroup Types ==============
export interface PromoGroup {
id: number
name: string
server_discount_percent: number
traffic_discount_percent: number
device_discount_percent: number
period_discounts: Record<number, number>
auto_assign_total_spent_kopeks: number | null
apply_discounts_to_addons: boolean
is_default: boolean
members_count: number
created_at: string | null
updated_at: string | null
id: number;
name: string;
server_discount_percent: number;
traffic_discount_percent: number;
device_discount_percent: number;
period_discounts: Record<number, number>;
auto_assign_total_spent_kopeks: number | null;
apply_discounts_to_addons: boolean;
is_default: boolean;
members_count: number;
created_at: string | null;
updated_at: string | null;
}
export interface PromoGroupListResponse {
items: PromoGroup[]
total: number
limit: number
offset: number
items: PromoGroup[];
total: number;
limit: number;
offset: number;
}
export interface PromoGroupCreateRequest {
name: string
server_discount_percent?: number
traffic_discount_percent?: number
device_discount_percent?: number
period_discounts?: Record<number, number>
auto_assign_total_spent_kopeks?: number | null
apply_discounts_to_addons?: boolean
is_default?: boolean
name: string;
server_discount_percent?: number;
traffic_discount_percent?: number;
device_discount_percent?: number;
period_discounts?: Record<number, number>;
auto_assign_total_spent_kopeks?: number | null;
apply_discounts_to_addons?: boolean;
is_default?: boolean;
}
export interface PromoGroupUpdateRequest {
name?: string
server_discount_percent?: number
traffic_discount_percent?: number
device_discount_percent?: number
period_discounts?: Record<number, number>
auto_assign_total_spent_kopeks?: number | null
apply_discounts_to_addons?: boolean
is_default?: boolean
name?: string;
server_discount_percent?: number;
traffic_discount_percent?: number;
device_discount_percent?: number;
period_discounts?: Record<number, number>;
auto_assign_total_spent_kopeks?: number | null;
apply_discounts_to_addons?: boolean;
is_default?: boolean;
}
// ============== API ==============
@@ -124,58 +129,58 @@ export interface PromoGroupUpdateRequest {
export const promocodesApi = {
// Promocodes
getPromocodes: async (params?: {
limit?: number
offset?: number
is_active?: boolean
limit?: number;
offset?: number;
is_active?: boolean;
}): Promise<PromoCodeListResponse> => {
const response = await apiClient.get('/cabinet/admin/promocodes', { params })
return response.data
const response = await apiClient.get('/cabinet/admin/promocodes', { params });
return response.data;
},
getPromocode: async (id: number): Promise<PromoCodeDetail> => {
const response = await apiClient.get(`/cabinet/admin/promocodes/${id}`)
return response.data
const response = await apiClient.get(`/cabinet/admin/promocodes/${id}`);
return response.data;
},
createPromocode: async (data: PromoCodeCreateRequest): Promise<PromoCode> => {
const response = await apiClient.post('/cabinet/admin/promocodes', data)
return response.data
const response = await apiClient.post('/cabinet/admin/promocodes', data);
return response.data;
},
updatePromocode: async (id: number, data: PromoCodeUpdateRequest): Promise<PromoCode> => {
const response = await apiClient.patch(`/cabinet/admin/promocodes/${id}`, data)
return response.data
const response = await apiClient.patch(`/cabinet/admin/promocodes/${id}`, data);
return response.data;
},
deletePromocode: async (id: number): Promise<void> => {
await apiClient.delete(`/cabinet/admin/promocodes/${id}`)
await apiClient.delete(`/cabinet/admin/promocodes/${id}`);
},
// Promo Groups
getPromoGroups: async (params?: {
limit?: number
offset?: number
limit?: number;
offset?: number;
}): Promise<PromoGroupListResponse> => {
const response = await apiClient.get('/cabinet/admin/promo-groups', { params })
return response.data
const response = await apiClient.get('/cabinet/admin/promo-groups', { params });
return response.data;
},
getPromoGroup: async (id: number): Promise<PromoGroup> => {
const response = await apiClient.get(`/cabinet/admin/promo-groups/${id}`)
return response.data
const response = await apiClient.get(`/cabinet/admin/promo-groups/${id}`);
return response.data;
},
createPromoGroup: async (data: PromoGroupCreateRequest): Promise<PromoGroup> => {
const response = await apiClient.post('/cabinet/admin/promo-groups', data)
return response.data
const response = await apiClient.post('/cabinet/admin/promo-groups', data);
return response.data;
},
updatePromoGroup: async (id: number, data: PromoGroupUpdateRequest): Promise<PromoGroup> => {
const response = await apiClient.patch(`/cabinet/admin/promo-groups/${id}`, data)
return response.data
const response = await apiClient.patch(`/cabinet/admin/promo-groups/${id}`, data);
return response.data;
},
deletePromoGroup: async (id: number): Promise<void> => {
await apiClient.delete(`/cabinet/admin/promo-groups/${id}`)
await apiClient.delete(`/cabinet/admin/promo-groups/${id}`);
},
}
};

View File

@@ -1,62 +1,65 @@
import apiClient from './client'
import type { ReferralInfo, ReferralTerms, PaginatedResponse } from '../types'
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
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
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
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
const response = await apiClient.get<ReferralInfo>('/cabinet/referral');
return response.data;
},
// Get referral list
getReferralList: async (params?: {
page?: number
per_page?: number
page?: number;
per_page?: number;
}): Promise<PaginatedResponse<ReferralItem>> => {
const response = await apiClient.get<PaginatedResponse<ReferralItem>>('/cabinet/referral/list', {
params,
})
return response.data
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
page?: number;
per_page?: number;
}): Promise<ReferralEarningsList> => {
const response = await apiClient.get<ReferralEarningsList>('/cabinet/referral/earnings', {
params,
})
return response.data
});
return response.data;
},
// Get referral terms
getReferralTerms: async (): Promise<ReferralTerms> => {
const response = await apiClient.get<ReferralTerms>('/cabinet/referral/terms')
return response.data
const response = await apiClient.get<ReferralTerms>('/cabinet/referral/terms');
return response.data;
},
}
};

View File

@@ -1,142 +1,142 @@
import apiClient from './client'
import apiClient from './client';
// Types
export interface PromoGroupInfo {
id: number
name: string
is_selected: boolean
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
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
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
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[]
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
id: number;
is_available: boolean;
message: string;
}
export interface ServerTrialToggleResponse {
id: number
is_trial_eligible: boolean
message: string
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
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
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
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
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
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
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
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
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
const response = await apiClient.post('/cabinet/admin/servers/sync');
return response.data;
},
}
};

View File

@@ -1,218 +1,256 @@
import apiClient from './client'
import type { Subscription, RenewalOption, TrafficPackage, TrialInfo, PurchaseOptions, PurchaseSelection, PurchasePreview, AppConfig } from '../types'
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
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
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
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
});
return response.data;
},
// Get traffic packages
getTrafficPackages: async (): Promise<TrafficPackage[]> => {
const response = await apiClient.get<TrafficPackage[]>('/cabinet/subscription/traffic-packages')
return response.data
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
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
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
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
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
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
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
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
});
return response.data;
},
// Get trial info
getTrialInfo: async (): Promise<TrialInfo> => {
const response = await apiClient.get<TrialInfo>('/cabinet/subscription/trial')
return response.data
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
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
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
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
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
});
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
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
});
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
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
base_price_kopeks: number
price_kopeks: number
price_per_month_kopeks: number
price_rubles: number
is_available: boolean
is_connected: boolean
has_discount: boolean
discount_percent: number
}>
connected_count: number
has_subscription: boolean
days_left: number
discount_percent: number
uuid: string;
name: string;
country_code: string | null;
base_price_kopeks: number;
price_kopeks: number;
price_per_month_kopeks: number;
price_rubles: number;
is_available: boolean;
is_connected: boolean;
has_discount: boolean;
discount_percent: number;
}>;
connected_count: number;
has_subscription: boolean;
days_left: number;
discount_percent: number;
}> => {
const response = await apiClient.get('/cabinet/subscription/countries')
return response.data
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[]
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
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
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[]
}
steps: string[];
};
}> => {
const response = await apiClient.get('/cabinet/subscription/connection-link')
return response.data
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
platforms: Record<
string,
{
name: string;
icon: string;
link: string;
}
>;
happ_enabled: boolean;
}> => {
const response = await apiClient.get('/cabinet/subscription/happ-downloads')
return response.data
const response = await apiClient.get('/cabinet/subscription/happ-downloads');
return response.data;
},
// ============ Device Management ============
@@ -220,130 +258,140 @@ export const subscriptionApi = {
// Get connected devices
getDevices: async (): Promise<{
devices: Array<{
hwid: string
platform: string
device_model: string
created_at: string | null
}>
total: number
device_limit: number
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
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
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
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
success: boolean;
message: string;
deleted_count: number;
}> => {
const response = await apiClient.delete('/cabinet/subscription/devices')
return response.data
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
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
});
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
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
});
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
success: boolean;
message: string;
is_paused: boolean;
balance_kopeks: number;
balance_label: string;
}> => {
const response = await apiClient.post('/cabinet/subscription/pause')
return response.data
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
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
const response = await apiClient.put('/cabinet/subscription/traffic', { gb });
return response.data;
},
// Refresh traffic usage from RemnaWave (rate limited: 1 per 60 seconds)
refreshTraffic: async (): Promise<{
success: boolean
cached: boolean
rate_limited?: boolean
retry_after_seconds?: number
source?: string
traffic_used_bytes: number
traffic_used_gb: number
traffic_limit_bytes: number
traffic_limit_gb: number
traffic_used_percent: number
is_unlimited: boolean
lifetime_used_bytes?: number
lifetime_used_gb?: number
success: boolean;
cached: boolean;
rate_limited?: boolean;
retry_after_seconds?: number;
source?: string;
traffic_used_bytes: number;
traffic_used_gb: number;
traffic_limit_bytes: number;
traffic_limit_gb: number;
traffic_used_percent: number;
is_unlimited: boolean;
lifetime_used_bytes?: number;
lifetime_used_gb?: number;
}> => {
const response = await apiClient.post('/cabinet/subscription/refresh-traffic')
return response.data
const response = await apiClient.post('/cabinet/subscription/refresh-traffic');
return response.data;
},
}
};

View File

@@ -1,239 +1,239 @@
import apiClient from './client'
import apiClient from './client';
// Types
export interface PeriodPrice {
days: number
price_kopeks: number
price_rubles?: number
days: number;
price_kopeks: number;
price_rubles?: number;
}
export interface ServerTrafficLimit {
traffic_limit_gb: number
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
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
id: number;
name: string;
is_selected: boolean;
}
export interface TariffListItem {
id: number
name: string
description: string | null
is_active: boolean
is_trial_available: boolean
is_daily: boolean
daily_price_kopeks: number
traffic_limit_gb: number
device_limit: number
tier_level: number
display_order: number
servers_count: number
subscriptions_count: number
created_at: string
id: number;
name: string;
description: string | null;
is_active: boolean;
is_trial_available: boolean;
is_daily: boolean;
daily_price_kopeks: number;
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
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
max_device_limit: 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
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;
max_device_limit: 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_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
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
traffic_topup_enabled: boolean;
traffic_topup_packages: Record<string, number>;
max_topup_traffic_gb: number;
// Дневной тариф
is_daily: boolean
daily_price_kopeks: number
is_daily: boolean;
daily_price_kopeks: number;
// Режим сброса трафика
traffic_reset_mode: string | null // 'DAY', 'WEEK', 'MONTH', 'NO_RESET', null = глобальная настройка
created_at: string
updated_at: string | null
traffic_reset_mode: string | null; // 'DAY', 'WEEK', 'MONTH', 'NO_RESET', null = глобальная настройка
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
max_device_limit?: number
tier_level?: number
period_prices?: PeriodPrice[]
allowed_squads?: string[]
server_traffic_limits?: Record<string, ServerTrafficLimit>
promo_group_ids?: number[]
name: string;
description?: string;
is_active?: boolean;
traffic_limit_gb?: number;
device_limit?: number;
device_price_kopeks?: number;
max_device_limit?: 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_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
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
traffic_topup_enabled?: boolean;
traffic_topup_packages?: Record<string, number>;
max_topup_traffic_gb?: number;
// Дневной тариф
is_daily?: boolean
daily_price_kopeks?: number
is_daily?: boolean;
daily_price_kopeks?: number;
// Режим сброса трафика
traffic_reset_mode?: string | null
traffic_reset_mode?: string | null;
}
export interface TariffUpdateRequest {
name?: string
description?: string
is_active?: boolean
traffic_limit_gb?: number
device_limit?: number
device_price_kopeks?: number
max_device_limit?: number
tier_level?: number
display_order?: number
period_prices?: PeriodPrice[]
allowed_squads?: string[]
server_traffic_limits?: Record<string, ServerTrafficLimit>
promo_group_ids?: number[]
name?: string;
description?: string;
is_active?: boolean;
traffic_limit_gb?: number;
device_limit?: number;
device_price_kopeks?: number;
max_device_limit?: 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_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
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
traffic_topup_enabled?: boolean;
traffic_topup_packages?: Record<string, number>;
max_topup_traffic_gb?: number;
// Дневной тариф
is_daily?: boolean
daily_price_kopeks?: number
is_daily?: boolean;
daily_price_kopeks?: number;
// Режим сброса трафика
traffic_reset_mode?: string | null
traffic_reset_mode?: string | null;
}
export interface TariffToggleResponse {
id: number
is_active: boolean
message: string
id: number;
is_active: boolean;
message: string;
}
export interface TariffTrialResponse {
id: number
is_trial_available: boolean
message: string
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
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
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
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
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
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
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
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
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
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
const response = await apiClient.get('/cabinet/admin/tariffs/available-servers');
return response.data;
},
}
};

View File

@@ -1,43 +1,48 @@
import apiClient from './client'
import { ThemeSettings, DEFAULT_THEME_COLORS, EnabledThemes, DEFAULT_ENABLED_THEMES } from '../types/theme'
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
const response = await apiClient.get<ThemeSettings>('/cabinet/branding/colors');
return response.data;
} catch {
// Return default colors if endpoint not available
return DEFAULT_THEME_COLORS
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
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
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
const response = await apiClient.get<EnabledThemes>('/cabinet/branding/themes');
return response.data;
} catch {
return DEFAULT_ENABLED_THEMES
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
const response = await apiClient.patch<EnabledThemes>('/cabinet/branding/themes', themes);
return response.data;
},
}
};

View File

@@ -1,64 +1,80 @@
import apiClient from './client'
import type { TicketNotificationList, UnreadCountResponse } from '../types'
import apiClient from './client';
import type { TicketNotificationList, UnreadCountResponse } from '../types';
export const ticketNotificationsApi = {
// User notifications
getNotifications: async (unreadOnly = false, limit = 50, offset = 0): Promise<TicketNotificationList> => {
getNotifications: async (
unreadOnly = false,
limit = 50,
offset = 0,
): Promise<TicketNotificationList> => {
const response = await apiClient.get('/cabinet/tickets/notifications', {
params: { unread_only: unreadOnly, limit, offset }
})
return response.data
params: { unread_only: unreadOnly, limit, offset },
});
return response.data;
},
getUnreadCount: async (): Promise<UnreadCountResponse> => {
const response = await apiClient.get('/cabinet/tickets/notifications/unread-count')
return response.data
const response = await apiClient.get('/cabinet/tickets/notifications/unread-count');
return response.data;
},
markAsRead: async (notificationId: number): Promise<{ success: boolean }> => {
const response = await apiClient.post(`/cabinet/tickets/notifications/${notificationId}/read`)
return response.data
const response = await apiClient.post(`/cabinet/tickets/notifications/${notificationId}/read`);
return response.data;
},
markAllAsRead: async (): Promise<{ success: boolean; marked_count: number }> => {
const response = await apiClient.post('/cabinet/tickets/notifications/read-all')
return response.data
const response = await apiClient.post('/cabinet/tickets/notifications/read-all');
return response.data;
},
markTicketAsRead: async (ticketId: number): Promise<{ success: boolean; marked_count: number }> => {
const response = await apiClient.post(`/cabinet/tickets/notifications/ticket/${ticketId}/read`)
return response.data
markTicketAsRead: async (
ticketId: number,
): Promise<{ success: boolean; marked_count: number }> => {
const response = await apiClient.post(`/cabinet/tickets/notifications/ticket/${ticketId}/read`);
return response.data;
},
// Admin notifications
getAdminNotifications: async (unreadOnly = false, limit = 50, offset = 0): Promise<TicketNotificationList> => {
const params: Record<string, unknown> = { limit, offset }
getAdminNotifications: async (
unreadOnly = false,
limit = 50,
offset = 0,
): Promise<TicketNotificationList> => {
const params: Record<string, unknown> = { limit, offset };
if (unreadOnly) {
params.unread_only = true
params.unread_only = true;
}
const response = await apiClient.get('/cabinet/admin/tickets/notifications', { params })
return response.data
const response = await apiClient.get('/cabinet/admin/tickets/notifications', { params });
return response.data;
},
getAdminUnreadCount: async (): Promise<UnreadCountResponse> => {
const response = await apiClient.get('/cabinet/admin/tickets/notifications/unread-count')
return response.data
const response = await apiClient.get('/cabinet/admin/tickets/notifications/unread-count');
return response.data;
},
markAdminAsRead: async (notificationId: number): Promise<{ success: boolean }> => {
const response = await apiClient.post(`/cabinet/admin/tickets/notifications/${notificationId}/read`)
return response.data
const response = await apiClient.post(
`/cabinet/admin/tickets/notifications/${notificationId}/read`,
);
return response.data;
},
markAllAdminAsRead: async (): Promise<{ success: boolean; marked_count: number }> => {
const response = await apiClient.post('/cabinet/admin/tickets/notifications/read-all')
return response.data
const response = await apiClient.post('/cabinet/admin/tickets/notifications/read-all');
return response.data;
},
markAdminTicketAsRead: async (ticketId: number): Promise<{ success: boolean; marked_count: number }> => {
const response = await apiClient.post(`/cabinet/admin/tickets/notifications/ticket/${ticketId}/read`)
return response.data
markAdminTicketAsRead: async (
ticketId: number,
): Promise<{ success: boolean; marked_count: number }> => {
const response = await apiClient.post(
`/cabinet/admin/tickets/notifications/ticket/${ticketId}/read`,
);
return response.data;
},
}
};
export default ticketNotificationsApi
export default ticketNotificationsApi;

View File

@@ -1,84 +1,84 @@
import apiClient from './client'
import type { Ticket, TicketDetail, TicketMessage, PaginatedResponse } from '../types'
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_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
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
page?: number;
per_page?: number;
status?: string;
}): Promise<PaginatedResponse<Ticket>> => {
const response = await apiClient.get<PaginatedResponse<Ticket>>('/cabinet/tickets', {
params,
})
return response.data
});
return response.data;
},
// Create ticket with optional media
createTicket: async (
title: string,
message: string,
media?: MediaParams
media?: MediaParams,
): Promise<TicketDetail> => {
const response = await apiClient.post<TicketDetail>('/cabinet/tickets', {
title,
message,
...media,
})
return response.data
});
return response.data;
},
// Get ticket detail
getTicket: async (ticketId: number): Promise<TicketDetail> => {
const response = await apiClient.get<TicketDetail>(`/cabinet/tickets/${ticketId}`)
return response.data
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
media?: MediaParams,
): Promise<TicketMessage> => {
const response = await apiClient.post<TicketMessage>(`/cabinet/tickets/${ticketId}/messages`, {
message,
...media,
})
return response.data
});
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 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
});
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}`
const baseUrl = import.meta.env.VITE_API_URL || '';
return `${baseUrl}/cabinet/media/${fileId}`;
},
}
};

View File

@@ -1,182 +1,182 @@
import apiClient from './client'
import apiClient from './client';
// ==================== TYPES ====================
export interface WheelPrize {
id: number
display_name: string
emoji: string
color: string
prize_type: string
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
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
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
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
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
items: SpinHistoryItem[];
total: number;
page: number;
per_page: number;
pages: number;
}
export interface StarsInvoiceResponse {
invoice_url: string
stars_amount: number
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
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;
}
// Type for creating a new prize (excludes id, config_id which are auto-generated)
export interface CreateWheelPrizeData {
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
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;
}
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
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 }>
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
}>
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
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
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
items: AdminSpinItem[];
total: number;
page: number;
per_page: number;
pages: number;
}
// ==================== USER API ====================
@@ -184,101 +184,107 @@ export interface AdminSpinsResponse {
export const wheelApi = {
// Get wheel config
getConfig: async (): Promise<WheelConfig> => {
const response = await apiClient.get<WheelConfig>('/cabinet/wheel/config')
return response.data
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
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
});
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
});
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
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
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
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
const response = await apiClient.get<WheelPrizeAdmin[]>('/cabinet/admin/wheel/prizes');
return response.data;
},
// Create prize
createPrize: async (data: CreateWheelPrizeData): Promise<WheelPrizeAdmin> => {
const response = await apiClient.post<WheelPrizeAdmin>('/cabinet/admin/wheel/prizes', data)
return response.data
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
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}`)
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 })
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
});
return response.data;
},
// Get all spins
getSpins: async (params?: {
user_id?: number
date_from?: string
date_to?: string
page?: number
per_page?: number
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
});
return response.data;
},
}
};