feat(api, web): интеграция хостера 4VPS.SU — синк VPS, баланса и тарифов
Docker / build (push) Has been cancelled

Добавлен адаптер 4vps, обобщён pipeline синхронизации через ProviderAdapter и обновлён UI для Panel ID и API Key.

Co-authored-by: Cursor <[email protected]>
This commit is contained in:
Denozordec
2026-06-29 01:13:50 +07:00
co-authored by Cursor
parent 05e7bf829e
commit 9be50aa03b
33 changed files with 1564 additions and 174 deletions
+52
View File
@@ -37,9 +37,41 @@ CREATE TABLE IF NOT EXISTS provider_accounts (
CREATE TABLE IF NOT EXISTS vps (
id TEXT PRIMARY KEY,
ip TEXT,
ipv6 TEXT,
additionalIps TEXT,
dns TEXT,
providerId TEXT,
providerAccountId TEXT,
country TEXT,
city TEXT,
datacenter TEXT,
os TEXT,
vcpu INTEGER,
ramGb REAL,
diskGb INTEGER,
diskType TEXT,
virtualization TEXT,
bandwidthTb INTEGER,
sshPort INTEGER,
rootUser TEXT,
purpose TEXT,
environment TEXT,
project TEXT,
projectId TEXT,
monitoringEnabled INTEGER,
backupEnabled INTEGER,
status TEXT,
tariffType TEXT,
currency TEXT,
dailyRate REAL,
monthlyRate REAL,
createdAt TEXT,
paidUntil TEXT,
notes TEXT,
userOverrides TEXT,
customData TEXT,
last_health_status TEXT,
last_health_checked_at TEXT,
FOREIGN KEY (providerId) REFERENCES providers(id),
FOREIGN KEY (providerAccountId) REFERENCES provider_accounts(id)
);
@@ -75,6 +107,10 @@ CREATE TABLE IF NOT EXISTS sync_log (
startedAt TEXT NOT NULL,
finishedAt TEXT,
status TEXT,
vpsCount INTEGER,
paymentsCount INTEGER,
error TEXT,
summary TEXT,
FOREIGN KEY (accountId) REFERENCES provider_accounts(id)
);
@@ -83,6 +119,22 @@ CREATE TABLE IF NOT EXISTS active_tariffs (
providerAccountId TEXT NOT NULL,
providerId TEXT NOT NULL,
externalId TEXT NOT NULL,
datacenterKey TEXT,
datacenterName TEXT,
name TEXT,
desc TEXT,
vcpu INTEGER,
ramGb REAL,
diskGb INTEGER,
diskType TEXT,
virtualization TEXT,
channel TEXT,
location TEXT,
country TEXT,
cpuModel TEXT,
orderAvailable INTEGER,
price TEXT,
syncedAt TEXT,
FOREIGN KEY (providerAccountId) REFERENCES provider_accounts(id),
FOREIGN KEY (providerId) REFERENCES providers(id)
);
+13 -1
View File
@@ -1,5 +1,17 @@
import { z } from 'zod'
export const API_TYPES = ['billmanager', '4vps', 'none'] as const
export type ApiType = (typeof API_TYPES)[number]
export const apiTypeSchema = z.enum(API_TYPES).optional().default('none')
export const SYNC_API_TYPES = ['billmanager', '4vps'] as const
export type SyncApiType = (typeof SYNC_API_TYPES)[number]
export function isSyncApiType(apiType?: string | null): apiType is SyncApiType {
const key = String(apiType || '').toLowerCase()
return (SYNC_API_TYPES as readonly string[]).includes(key)
}
export const providerSchema = z.object({
id: z.string().optional(),
name: z.string().min(1, 'Name is required'),
@@ -9,7 +21,7 @@ export const providerSchema = z.object({
usdRate: z.string().optional().default(''),
eurRate: z.string().optional().default(''),
notes: z.string().optional().default(''),
apiType: z.string().optional().default(''),
apiType: apiTypeSchema,
apiBaseUrl: z.string().optional().default(''),
})
@@ -13,3 +13,32 @@ export function buildApiCredentials(login: string, password: string): string {
if (!l) return p
return p ? `${l}:${p}` : ''
}
export interface FourVpsCredentials {
panelId: number | null
apiKey: string
}
/** Разбор 4VPS-кредов формата `panelId:apiKey` (panelId опционален). */
export function parseFourVpsCredentials(credentials: string | null | undefined): FourVpsCredentials {
const cred = String(credentials ?? '').trim()
if (!cred) return { panelId: null, apiKey: '' }
const idx = cred.indexOf(':')
if (idx <= 0) return { panelId: null, apiKey: cred }
const panelPart = cred.slice(0, idx).trim()
const apiKey = cred.slice(idx + 1)
const panelId = panelPart ? Number.parseInt(panelPart, 10) : null
return {
panelId: panelId != null && Number.isFinite(panelId) ? panelId : null,
apiKey,
}
}
/** Собрать 4VPS-креды: panelId + API key. */
export function buildFourVpsCredentials(panelId: string, apiKey: string): string {
const pid = panelId.trim()
const key = apiKey
if (!pid && !key) return ''
if (!pid) return key
return key ? `${pid}:${key}` : pid
}
@@ -1,4 +1,5 @@
import { accountBalanceApi } from './account-balance.js'
import { isSyncApiType } from '../contracts/provider.js'
import { getPaidUntilDate, type PaidUntilContext } from './paid-until.js'
export const STALE_SYNC_HOURS = 48
@@ -166,12 +167,12 @@ export function computeInventoryHealth(input: InventoryHealthInput): InventoryIs
})
}
const bmAccounts = providerAccounts.filter((a) => {
const syncAccounts = providerAccounts.filter((a) => {
const p = providerById.get(a.providerId)
return p?.apiType === 'billmanager' && Boolean((p.apiBaseUrl || '').trim()) && a.apiCredentialsSet
return isSyncApiType(p?.apiType) && Boolean((p?.apiBaseUrl || '').trim()) && a.apiCredentialsSet
})
const staleMs = STALE_SYNC_HOURS * 60 * 60 * 1000
const staleAccounts = bmAccounts.filter((a) => {
const staleAccounts = syncAccounts.filter((a) => {
const t = lastOkSyncFinishedAt(a.id, syncLog)
if (t == null) return true
return now.getTime() - t > staleMs
@@ -226,12 +227,12 @@ export function getStaleSyncAccountIds(
now = new Date(),
): string[] {
const providerById = new Map(providers.map((p) => [p.id, p]))
const bmAccounts = providerAccounts.filter((a) => {
const syncAccounts = providerAccounts.filter((a) => {
const p = providerById.get(a.providerId)
return p?.apiType === 'billmanager' && Boolean((p.apiBaseUrl || '').trim()) && a.apiCredentialsSet
return isSyncApiType(p?.apiType) && Boolean((p?.apiBaseUrl || '').trim()) && a.apiCredentialsSet
})
const staleMs = STALE_SYNC_HOURS * 60 * 60 * 1000
return bmAccounts
return syncAccounts
.filter((a) => {
const t = lastOkSyncFinishedAt(a.id, syncLog)
if (t == null) return true
+2 -1
View File
@@ -11,5 +11,6 @@
"declarationMap": true,
"verbatimModuleSyntax": false
},
"include": ["src/**/*.ts"]
"include": ["src/**/*.ts"],
"exclude": ["src/**/*.test.ts"]
}