feat(api, web): интеграция Macloud и VDSina через унифицированный UserAPI
Docker / build (push) Has been cancelled

Один модуль userapi для идентичного API обоих хостеров: синк VPS, баланса, тарифов и пополнений.

Co-authored-by: Cursor <[email protected]>
This commit is contained in:
Denozordec
2026-06-29 02:02:03 +07:00
co-authored by Cursor
parent 9be50aa03b
commit f2c4c7a412
18 changed files with 1469 additions and 32 deletions
@@ -14,7 +14,7 @@ import { providerAccountSchema, type ProviderAccountFormValues } from '@/lib/sch
import type { BillingMode, Provider } from '@/types/entities'
import { billingModeLabel } from '@/lib/format'
import { api, ApiError } from '@/lib/api-client'
import { accountCredentialLabels } from '@/lib/provider-sync'
import { accountCredentialLabels, isUserApiType } from '@/lib/provider-sync'
const EMPTY: ProviderAccountFormValues = {
providerId: '',
@@ -84,9 +84,11 @@ export function ProviderAccountEditSheet({
onOpenChange={onOpenChange}
title={isEdit ? 'Редактировать аккаунт' : 'Новый аккаунт'}
description={
initialProvider?.apiType === '4vps'
? 'Panel ID и API Key хранятся на сервере и используются для синка с 4VPS'
: 'API-креды хранятся на сервере и используются для синка с BILLmanager'
isUserApiType(initialProvider?.apiType)
? 'API Token хранится на сервере и используется для синка с UserAPI'
: initialProvider?.apiType === '4vps'
? 'Panel ID и API Key хранятся на сервере и используются для синка с 4VPS'
: 'API-креды хранятся на сервере и используются для синка с BILLmanager'
}
schema={providerAccountSchema as unknown as ZodType<ProviderAccountFormValues>}
defaultValues={defaultValues}
@@ -103,6 +105,7 @@ export function ProviderAccountEditSheet({
const apiCredentials = buildApiCredentials(apiLogin, apiPassword)
const canTest = Boolean(apiBaseUrl && apiCredentials)
const credLabels = accountCredentialLabels(provider?.apiType)
const tokenOnly = isUserApiType(provider?.apiType)
return (
<>
@@ -118,14 +121,16 @@ export function ProviderAccountEditSheet({
<FormField label="Название" htmlFor="acc-name" error={errors.name?.message} invalid={!!errors.name}>
<Input id="acc-name" aria-invalid={!!errors.name} {...register('name')} />
</FormField>
<FormField label={credLabels.loginLabel} htmlFor="acc-login">
<Input
id="acc-login"
autoComplete="off"
placeholder={credLabels.loginPlaceholder || undefined}
{...register('apiLogin')}
/>
</FormField>
{!tokenOnly ? (
<FormField label={credLabels.loginLabel} htmlFor="acc-login">
<Input
id="acc-login"
autoComplete="off"
placeholder={credLabels.loginPlaceholder || undefined}
{...register('apiLogin')}
/>
</FormField>
) : null}
<FormField
label={isEdit ? `${credLabels.passwordLabel} (необязательно)` : credLabels.passwordLabel}
htmlFor="acc-password"
@@ -6,6 +6,7 @@ import { SelectField } from '@/components/select-field'
import type { ZodType } from 'zod'
import { providerSchema, type ProviderFormValues } from '@/lib/schemas'
import type { ApiType } from '@/types/entities'
import { isUserApiType, USER_API_DEFAULT_BASE_URL } from '@cfdm/shared/contracts/provider'
const EMPTY: ProviderFormValues = {
name: '',
@@ -53,6 +54,20 @@ export function ProviderEditSheet({
>
{(form) => {
const { register, formState: { errors }, watch, setValue } = form
const apiType = watch('apiType')
const apiUrlHint =
apiType === '4vps'
? 'Базовый URL API, например https://4vps.su/api'
: isUserApiType(apiType)
? `Базовый URL UserAPI, например ${USER_API_DEFAULT_BASE_URL[apiType]}`
: 'Один URL на хостера для BILLmanager'
const apiUrlPlaceholder =
apiType === '4vps'
? 'https://4vps.su/api'
: isUserApiType(apiType)
? USER_API_DEFAULT_BASE_URL[apiType]
: undefined
return (
<>
<FormField label="Название" htmlFor="pr-name" error={errors.name?.message} invalid={!!errors.name}>
@@ -69,24 +84,14 @@ export function ProviderEditSheet({
options={[
{ value: 'billmanager', label: 'BILLmanager' },
{ value: '4vps', label: '4VPS.SU' },
{ value: 'macloud', label: 'Маклауд' },
{ value: 'vdsina', label: 'VDSina' },
{ value: 'none', label: 'Нет' },
]}
/>
</FormField>
<FormField
label="API URL"
htmlFor="pr-apiurl"
description={
watch('apiType') === '4vps'
? 'Базовый URL API, например https://4vps.su/api'
: 'Один URL на хостера для BILLmanager'
}
>
<Input
id="pr-apiurl"
placeholder={watch('apiType') === '4vps' ? 'https://4vps.su/api' : undefined}
{...register('apiBaseUrl')}
/>
<FormField label="API URL" htmlFor="pr-apiurl" description={apiUrlHint}>
<Input id="pr-apiurl" placeholder={apiUrlPlaceholder} {...register('apiBaseUrl')} />
</FormField>
<div className="grid grid-cols-3 gap-3">
<FormField label="Валюта" htmlFor="pr-cur">