feat(api, web): интеграция хостера 4VPS.SU — синк VPS, баланса и тарифов
Docker / build (push) Has been cancelled
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:
@@ -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)
|
||||
);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -11,5 +11,6 @@
|
||||
"declarationMap": true,
|
||||
"verbatimModuleSyntax": false
|
||||
},
|
||||
"include": ["src/**/*.ts"]
|
||||
"include": ["src/**/*.ts"],
|
||||
"exclude": ["src/**/*.test.ts"]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user