refactor(repo): переход на pnpm monorepo с shadcn/ui и Fastify+Drizzle
Frontend:
- apps/web (Vite+TS, TanStack Router/Query, shadcn/ui @cfdm/ui base-nova)
- 10 страниц в routes/_auth/, Recharts через shadcn Chart, lucide-react
- формы на RHF + Zod (FormSheet/FormField)
- удалены Tabler, Chart.js, react-router-dom
Backend (параллельный трек):
- apps/api (Fastify 5 + Drizzle + better-sqlite3)
- packages/db: Drizzle-схема и repositories по сущностям
- packages/shared: Zod-контракты
- роуты с валидацией и единым форматом ошибок { error: { code, message } }
- sync/backup — заглушки 501 (billmanager-адаптеры переносятся отдельно)
- legacy Express оставлен как runtime по умолчанию (RUNTIME=express)
Infra:
- Dockerfile multi-stage под pnpm workspaces
- .dockerignore и docker-compose обновлены под monorepo
Rules:
- удалены нерелевантные правила (rust, cloudflare, server/frontend-conventions)
- project-structure.mdc и AGENTS.md переписаны под monorepo
- frontend-shadcn.mdc, shadcn-ui-production.mdc, sqlite.mdc обновлены
Co-authored-by: Cursor <[email protected]>
This commit is contained in:
@@ -0,0 +1,348 @@
|
||||
import { Router } from 'express'
|
||||
import express from 'express'
|
||||
import { readFileSync, existsSync } from 'node:fs'
|
||||
import { getDb, saveDb, DB_PATH, reloadDatabaseFromBuffer } from '../db.js'
|
||||
import { consolidateAllProviderApiSources } from '../db/migrations.js'
|
||||
import { rowToVps } from './vps.js'
|
||||
import { rowToActiveTariff, rowToTariffSyncOptions } from '../utils/row-mappers.js'
|
||||
|
||||
const router = Router()
|
||||
const BACKUP_VERSION = 1
|
||||
|
||||
function buildJsonSnapshot() {
|
||||
saveDb()
|
||||
const db = getDb()
|
||||
const vps = db.prepare('SELECT * FROM vps ORDER BY createdAt DESC').all()
|
||||
const providers = db.prepare('SELECT * FROM providers ORDER BY name').all()
|
||||
const providerAccounts = db.prepare('SELECT * FROM provider_accounts ORDER BY name').all()
|
||||
const payments = db.prepare('SELECT * FROM payments ORDER BY date DESC').all()
|
||||
const balanceLedger = db.prepare('SELECT * FROM balance_ledger ORDER BY date DESC').all()
|
||||
const settingsRows = db.prepare('SELECT * FROM settings ORDER BY id').all()
|
||||
const activeTariffs = db.prepare('SELECT * FROM active_tariffs ORDER BY name').all()
|
||||
const tariffSyncOptions = db.prepare('SELECT * FROM tariff_sync_options').all()
|
||||
let serverProjects = []
|
||||
try {
|
||||
serverProjects = db
|
||||
.prepare('SELECT id, name, color, sortOrder, notes, createdAt FROM server_projects ORDER BY name')
|
||||
.all()
|
||||
} catch {
|
||||
serverProjects = []
|
||||
}
|
||||
let syncLog = []
|
||||
try {
|
||||
syncLog = db.prepare('SELECT * FROM sync_log ORDER BY startedAt DESC LIMIT 500').all()
|
||||
} catch {
|
||||
syncLog = []
|
||||
}
|
||||
|
||||
return {
|
||||
backupVersion: BACKUP_VERSION,
|
||||
exportedAt: new Date().toISOString(),
|
||||
vps: vps.map(rowToVps),
|
||||
serverProjects,
|
||||
providers,
|
||||
providerAccounts,
|
||||
payments,
|
||||
balanceLedger,
|
||||
settings: settingsRows,
|
||||
activeTariffs: activeTariffs.map(rowToActiveTariff),
|
||||
tariffSyncOptions: tariffSyncOptions.map(rowToTariffSyncOptions),
|
||||
syncLog,
|
||||
}
|
||||
}
|
||||
|
||||
router.get('/json', (req, res) => {
|
||||
try {
|
||||
const snapshot = buildJsonSnapshot()
|
||||
res.setHeader('Content-Type', 'application/json; charset=utf-8')
|
||||
res.setHeader('Content-Disposition', 'attachment; filename="vps-tracker-backup.json"')
|
||||
res.send(JSON.stringify(snapshot, null, 2))
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message })
|
||||
}
|
||||
})
|
||||
|
||||
router.get('/database', (req, res) => {
|
||||
try {
|
||||
saveDb()
|
||||
if (!existsSync(DB_PATH)) {
|
||||
return res.status(404).json({ error: 'Файл базы не найден' })
|
||||
}
|
||||
const buf = readFileSync(DB_PATH)
|
||||
res.setHeader('Content-Type', 'application/octet-stream')
|
||||
res.setHeader('Content-Disposition', 'attachment; filename="vps-tracker.db"')
|
||||
res.send(buf)
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message })
|
||||
}
|
||||
})
|
||||
|
||||
router.post('/json', (req, res) => {
|
||||
try {
|
||||
const payload = req.body
|
||||
if (!payload || typeof payload !== 'object') {
|
||||
return res.status(400).json({ error: 'Неверное тело запроса' })
|
||||
}
|
||||
importJsonSnapshot(payload)
|
||||
res.json({ ok: true })
|
||||
} catch (err) {
|
||||
console.error('Backup JSON import error:', err)
|
||||
res.status(500).json({ error: err.message || 'Импорт не удался' })
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* @param {object} data
|
||||
*/
|
||||
function importJsonSnapshot(data) {
|
||||
const db = getDb()
|
||||
const run = (sql, ...params) => db.prepare(sql).run(...params)
|
||||
|
||||
run('DELETE FROM sync_log')
|
||||
run('DELETE FROM tariff_sync_options')
|
||||
run('DELETE FROM active_tariffs')
|
||||
run('DELETE FROM balance_ledger')
|
||||
run('DELETE FROM payments')
|
||||
run('DELETE FROM vps')
|
||||
run('DELETE FROM provider_accounts')
|
||||
run('DELETE FROM server_projects')
|
||||
run('DELETE FROM providers')
|
||||
run('DELETE FROM settings')
|
||||
|
||||
const providers = Array.isArray(data.providers) ? data.providers : []
|
||||
for (const p of providers) {
|
||||
run(
|
||||
`INSERT OR REPLACE INTO providers (id, name, website, contact, baseCurrency, usdRate, eurRate, notes, apiType, apiBaseUrl) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
p.id ?? '',
|
||||
p.name ?? '',
|
||||
p.website ?? '',
|
||||
p.contact ?? '',
|
||||
p.baseCurrency ?? '',
|
||||
p.usdRate ?? '',
|
||||
p.eurRate ?? '',
|
||||
p.notes ?? '',
|
||||
p.apiType ?? '',
|
||||
p.apiBaseUrl ?? '',
|
||||
)
|
||||
}
|
||||
|
||||
const projects = Array.isArray(data.serverProjects) ? data.serverProjects : []
|
||||
for (const sp of projects) {
|
||||
run(
|
||||
`INSERT OR REPLACE INTO server_projects (id, name, color, sortOrder, notes, createdAt) VALUES (?, ?, ?, ?, ?, ?)`,
|
||||
sp.id ?? '',
|
||||
sp.name ?? '',
|
||||
sp.color ?? null,
|
||||
sp.sortOrder ?? 0,
|
||||
sp.notes ?? null,
|
||||
sp.createdAt ?? null,
|
||||
)
|
||||
}
|
||||
|
||||
const accounts = Array.isArray(data.providerAccounts) ? data.providerAccounts : []
|
||||
for (const acc of accounts) {
|
||||
run(
|
||||
`INSERT OR REPLACE INTO provider_accounts (id, providerId, name, panelUrl, currency, billingMode, notes, apiType, apiBaseUrl, apiCredentials, balance_api, balance_currency, balance_updated_at, enoughmoneyto, balance_alert_below)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
acc.id ?? '',
|
||||
acc.providerId ?? '',
|
||||
acc.name ?? '',
|
||||
acc.panelUrl ?? '',
|
||||
acc.currency ?? '',
|
||||
acc.billingMode ?? '',
|
||||
acc.notes ?? '',
|
||||
acc.apiType ?? '',
|
||||
acc.apiBaseUrl ?? '',
|
||||
acc.apiCredentials ?? '',
|
||||
acc.balance_api ?? null,
|
||||
acc.balance_currency ?? null,
|
||||
acc.balance_updated_at ?? null,
|
||||
acc.enoughmoneyto ?? null,
|
||||
acc.balance_alert_below != null && acc.balance_alert_below !== '' ? Number(acc.balance_alert_below) : null,
|
||||
)
|
||||
}
|
||||
|
||||
consolidateAllProviderApiSources(db)
|
||||
|
||||
const settingsList = Array.isArray(data.settings) ? data.settings : data.settings ? [data.settings] : []
|
||||
for (const s of settingsList) {
|
||||
let customFields = s.customFields
|
||||
if (Array.isArray(customFields)) customFields = JSON.stringify(customFields)
|
||||
if (customFields === undefined) customFields = null
|
||||
run(
|
||||
`INSERT OR REPLACE INTO settings (id, baseCurrency, ratesUrl, autoConvert, ratesUpdatedAt, syncEnabled, syncIntervalMinutes, syncTariffsIntervalMinutes, telegramBotToken, telegramChatId, telegramMessageThreadId, notifyPaymentExpiryEnabled, notifyNewTariffsEnabled, customFields, notifyLowBalanceEnabled, notifySyncDigestEnabled)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
s.id ?? 'settings-main',
|
||||
s.baseCurrency ?? 'RUB',
|
||||
s.ratesUrl ?? '',
|
||||
s.autoConvert !== false && s.autoConvert !== 0 ? 1 : 0,
|
||||
s.ratesUpdatedAt ?? '',
|
||||
s.syncEnabled ? 1 : 0,
|
||||
s.syncIntervalMinutes ?? 60,
|
||||
s.syncTariffsIntervalMinutes ?? 1440,
|
||||
s.telegramBotToken ?? '',
|
||||
s.telegramChatId ?? '',
|
||||
s.telegramMessageThreadId ?? '',
|
||||
s.notifyPaymentExpiryEnabled ? 1 : 0,
|
||||
s.notifyNewTariffsEnabled ? 1 : 0,
|
||||
customFields,
|
||||
s.notifyLowBalanceEnabled ? 1 : 0,
|
||||
s.notifySyncDigestEnabled ? 1 : 0,
|
||||
)
|
||||
}
|
||||
|
||||
const vpsRows = Array.isArray(data.vps) ? data.vps : []
|
||||
const vpsSql = `INSERT OR REPLACE INTO vps (id, ip, ipv6, additionalIps, dns, providerId, providerAccountId, country, city, datacenter, os, vcpu, ramGb, diskGb, diskType, virtualization, bandwidthTb, sshPort, rootUser, purpose, environment, project, projectId, monitoringEnabled, backupEnabled, status, tariffType, currency, dailyRate, monthlyRate, createdAt, paidUntil, notes, userOverrides)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
||||
for (const v of vpsRows) {
|
||||
const additionalIps = Array.isArray(v.additionalIps) ? JSON.stringify(v.additionalIps) : '[]'
|
||||
const dailyRate = v.dailyRate === '' || v.dailyRate == null ? null : Number(v.dailyRate)
|
||||
const monthlyRate = v.monthlyRate === '' || v.monthlyRate == null ? null : Number(v.monthlyRate)
|
||||
const userOverrides = Array.isArray(v.userOverrides) ? JSON.stringify(v.userOverrides) : (v.userOverrides ?? '[]')
|
||||
run(
|
||||
vpsSql,
|
||||
v.id ?? '',
|
||||
v.ip ?? '',
|
||||
v.ipv6 ?? '',
|
||||
additionalIps,
|
||||
v.dns ?? '',
|
||||
v.providerId ?? '',
|
||||
v.providerAccountId ?? '',
|
||||
v.country ?? '',
|
||||
v.city ?? '',
|
||||
v.datacenter ?? '',
|
||||
v.os ?? '',
|
||||
v.vcpu ?? 0,
|
||||
v.ramGb ?? 0,
|
||||
v.diskGb ?? 0,
|
||||
v.diskType ?? '',
|
||||
v.virtualization ?? '',
|
||||
v.bandwidthTb ?? 0,
|
||||
v.sshPort ?? 22,
|
||||
v.rootUser ?? '',
|
||||
v.purpose ?? '',
|
||||
v.environment ?? '',
|
||||
v.project ?? '',
|
||||
v.projectId ?? null,
|
||||
v.monitoringEnabled ? 1 : 0,
|
||||
v.backupEnabled ? 1 : 0,
|
||||
v.status ?? 'active',
|
||||
v.tariffType ?? '',
|
||||
v.currency ?? '',
|
||||
dailyRate,
|
||||
monthlyRate,
|
||||
v.createdAt ?? '',
|
||||
v.paidUntil ?? '',
|
||||
v.notes ?? '',
|
||||
userOverrides,
|
||||
)
|
||||
}
|
||||
|
||||
const payments = Array.isArray(data.payments) ? data.payments : []
|
||||
for (const pm of payments) {
|
||||
run(
|
||||
`INSERT OR REPLACE INTO payments (id, type, date, amount, currency, providerAccountId, vpsId, note) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
pm.id ?? '',
|
||||
pm.type ?? '',
|
||||
pm.date ?? '',
|
||||
Number(pm.amount) || 0,
|
||||
pm.currency ?? '',
|
||||
pm.providerAccountId ?? '',
|
||||
pm.vpsId ?? '',
|
||||
pm.note ?? '',
|
||||
)
|
||||
}
|
||||
|
||||
const ledger = Array.isArray(data.balanceLedger) ? data.balanceLedger : []
|
||||
for (const bl of ledger) {
|
||||
run(
|
||||
`INSERT OR REPLACE INTO balance_ledger (id, type, date, amount, currency, direction, providerAccountId, vpsId, note) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
bl.id ?? '',
|
||||
bl.type ?? '',
|
||||
bl.date ?? '',
|
||||
Number(bl.amount) || 0,
|
||||
bl.currency ?? '',
|
||||
bl.direction ?? '',
|
||||
bl.providerAccountId ?? '',
|
||||
bl.vpsId ?? '',
|
||||
bl.note ?? '',
|
||||
)
|
||||
}
|
||||
|
||||
const tariffs = Array.isArray(data.activeTariffs) ? data.activeTariffs : []
|
||||
for (const t of tariffs) {
|
||||
run(
|
||||
`INSERT OR REPLACE INTO active_tariffs (id, providerAccountId, providerId, externalId, datacenterKey, datacenterName, name, desc, vcpu, ramGb, diskGb, diskType, virtualization, channel, location, country, cpuModel, orderAvailable, price, syncedAt)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
t.id ?? '',
|
||||
t.providerAccountId ?? '',
|
||||
t.providerId ?? '',
|
||||
t.externalId ?? '',
|
||||
t.datacenterKey ?? '',
|
||||
t.datacenterName ?? '',
|
||||
t.name ?? '',
|
||||
t.desc ?? '',
|
||||
t.vcpu ?? 0,
|
||||
t.ramGb ?? 0,
|
||||
t.diskGb ?? 0,
|
||||
t.diskType ?? '',
|
||||
t.virtualization ?? '',
|
||||
t.channel ?? '',
|
||||
t.location ?? '',
|
||||
t.country ?? '',
|
||||
t.cpuModel ?? '',
|
||||
t.orderAvailable ? 1 : 0,
|
||||
t.price ?? '',
|
||||
t.syncedAt ?? '',
|
||||
)
|
||||
}
|
||||
|
||||
const tso = Array.isArray(data.tariffSyncOptions) ? data.tariffSyncOptions : []
|
||||
for (const o of tso) {
|
||||
const dcs = typeof o.datacenters === 'string' ? o.datacenters : JSON.stringify(o.datacenters || [])
|
||||
const pers = typeof o.periods === 'string' ? o.periods : JSON.stringify(o.periods || [])
|
||||
run(
|
||||
`INSERT OR REPLACE INTO tariff_sync_options (providerAccountId, datacenters, periods, syncedAt) VALUES (?, ?, ?, ?)`,
|
||||
o.providerAccountId ?? '',
|
||||
dcs,
|
||||
pers,
|
||||
o.syncedAt ?? '',
|
||||
)
|
||||
}
|
||||
|
||||
const logs = Array.isArray(data.syncLog) ? data.syncLog : []
|
||||
for (const log of logs) {
|
||||
run(
|
||||
`INSERT OR REPLACE INTO sync_log (id, accountId, startedAt, finishedAt, status, vpsCount, paymentsCount, error, summary) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
log.id ?? '',
|
||||
log.accountId ?? '',
|
||||
log.startedAt ?? '',
|
||||
log.finishedAt ?? null,
|
||||
log.status ?? '',
|
||||
log.vpsCount ?? null,
|
||||
log.paymentsCount ?? null,
|
||||
log.error ?? null,
|
||||
typeof log.summary === 'string' ? log.summary : log.summary ? JSON.stringify(log.summary) : null,
|
||||
)
|
||||
}
|
||||
|
||||
saveDb()
|
||||
}
|
||||
|
||||
router.post('/database', express.raw({ limit: '100mb', type: '*/*' }), async (req, res) => {
|
||||
try {
|
||||
const buf = Buffer.isBuffer(req.body) ? req.body : Buffer.from(req.body || [])
|
||||
if (!buf.length) {
|
||||
return res.status(400).json({ error: 'Пустой файл' })
|
||||
}
|
||||
await reloadDatabaseFromBuffer(buf)
|
||||
const { startScheduler } = await import('../sync-scheduler.js')
|
||||
startScheduler()
|
||||
res.json({ ok: true })
|
||||
} catch (err) {
|
||||
console.error('Backup DB restore error:', err)
|
||||
res.status(500).json({ error: err.message || 'Восстановление не удалось' })
|
||||
}
|
||||
})
|
||||
|
||||
export default router
|
||||
@@ -0,0 +1,82 @@
|
||||
import { Router } from 'express'
|
||||
import { getDb } from '../db.js'
|
||||
|
||||
const router = Router()
|
||||
|
||||
router.get('/', (req, res) => {
|
||||
try {
|
||||
const db = getDb()
|
||||
const rows = db.prepare('SELECT * FROM balance_ledger ORDER BY date DESC').all()
|
||||
res.json(rows)
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message })
|
||||
}
|
||||
})
|
||||
|
||||
router.post('/', (req, res) => {
|
||||
try {
|
||||
const db = getDb()
|
||||
const r = req.body
|
||||
const id = r.id || `ledger-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`
|
||||
db.prepare(`
|
||||
INSERT INTO balance_ledger (id, type, date, amount, currency, direction, providerAccountId, vpsId, note)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`).run(
|
||||
id,
|
||||
r.type ?? '',
|
||||
r.date ?? '',
|
||||
Number(r.amount) || 0,
|
||||
r.currency ?? '',
|
||||
r.direction ?? '',
|
||||
r.providerAccountId ?? '',
|
||||
r.vpsId ?? '',
|
||||
r.note ?? '',
|
||||
)
|
||||
const row = db.prepare('SELECT * FROM balance_ledger WHERE id = ?').get(id)
|
||||
res.status(201).json(row)
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message })
|
||||
}
|
||||
})
|
||||
|
||||
router.put('/:id', (req, res) => {
|
||||
try {
|
||||
const db = getDb()
|
||||
const { id } = req.params
|
||||
const r = req.body
|
||||
db.prepare(`
|
||||
UPDATE balance_ledger SET
|
||||
type = ?, date = ?, amount = ?, currency = ?, direction = ?, providerAccountId = ?, vpsId = ?, note = ?
|
||||
WHERE id = ?
|
||||
`).run(
|
||||
r.type ?? '',
|
||||
r.date ?? '',
|
||||
Number(r.amount) || 0,
|
||||
r.currency ?? '',
|
||||
r.direction ?? '',
|
||||
r.providerAccountId ?? '',
|
||||
r.vpsId ?? '',
|
||||
r.note ?? '',
|
||||
id,
|
||||
)
|
||||
const row = db.prepare('SELECT * FROM balance_ledger WHERE id = ?').get(id)
|
||||
if (!row) return res.status(404).json({ error: 'Not found' })
|
||||
res.json(row)
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message })
|
||||
}
|
||||
})
|
||||
|
||||
router.delete('/:id', (req, res) => {
|
||||
try {
|
||||
const db = getDb()
|
||||
const { id } = req.params
|
||||
const result = db.prepare('DELETE FROM balance_ledger WHERE id = ?').run(id)
|
||||
if (result.changes === 0) return res.status(404).json({ error: 'Not found' })
|
||||
res.status(204).send()
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message })
|
||||
}
|
||||
})
|
||||
|
||||
export default router
|
||||
@@ -0,0 +1,45 @@
|
||||
import { Router } from 'express'
|
||||
import { getDb } from '../db.js'
|
||||
import { rowToVps } from './vps.js'
|
||||
import { rowToSettings } from './settings.js'
|
||||
import { sanitizeAccount, rowToActiveTariff, rowToTariffSyncOptions } from '../utils/row-mappers.js'
|
||||
|
||||
const router = Router()
|
||||
|
||||
router.get('/', (req, res) => {
|
||||
try {
|
||||
const db = getDb()
|
||||
const vps = db.prepare('SELECT * FROM vps ORDER BY createdAt DESC').all()
|
||||
const providers = db.prepare('SELECT * FROM providers ORDER BY name').all()
|
||||
const providerAccounts = db.prepare('SELECT * FROM provider_accounts ORDER BY name').all()
|
||||
const payments = db.prepare('SELECT * FROM payments ORDER BY date DESC').all()
|
||||
const balanceLedger = db.prepare('SELECT * FROM balance_ledger ORDER BY date DESC').all()
|
||||
const settingsRows = db.prepare('SELECT * FROM settings ORDER BY id').all()
|
||||
const activeTariffs = db.prepare('SELECT * FROM active_tariffs ORDER BY name').all()
|
||||
const tariffSyncOptions = db.prepare('SELECT * FROM tariff_sync_options').all()
|
||||
let serverProjects = []
|
||||
try {
|
||||
serverProjects = db
|
||||
.prepare('SELECT id, name, color, sortOrder, notes, createdAt FROM server_projects ORDER BY name')
|
||||
.all()
|
||||
} catch {
|
||||
serverProjects = []
|
||||
}
|
||||
|
||||
res.json({
|
||||
vps: vps.map(rowToVps),
|
||||
serverProjects,
|
||||
providers,
|
||||
providerAccounts: providerAccounts.map(sanitizeAccount),
|
||||
payments,
|
||||
balanceLedger,
|
||||
settings: settingsRows.map(rowToSettings),
|
||||
activeTariffs: activeTariffs.map(rowToActiveTariff),
|
||||
tariffSyncOptions: tariffSyncOptions.map(rowToTariffSyncOptions),
|
||||
})
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message })
|
||||
}
|
||||
})
|
||||
|
||||
export default router
|
||||
@@ -0,0 +1,85 @@
|
||||
import { Router } from 'express'
|
||||
import { getDb, saveDb } from '../db.js'
|
||||
import { consolidateAllProviderApiSources } from '../db/migrations.js'
|
||||
|
||||
const router = Router()
|
||||
|
||||
router.post('/', (req, res) => {
|
||||
try {
|
||||
const db = getDb()
|
||||
const data = req.body
|
||||
if (!data || typeof data !== 'object') {
|
||||
return res.status(400).json({ error: 'Invalid payload' })
|
||||
}
|
||||
|
||||
const settingsList = Array.isArray(data.settings) ? data.settings : (data.settings ? [data.settings] : [])
|
||||
|
||||
if (Array.isArray(data.providers) && data.providers.length > 0) {
|
||||
const sql = `INSERT OR REPLACE INTO providers (id, name, website, contact, baseCurrency, usdRate, eurRate, notes, apiType, apiBaseUrl) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
||||
for (const r of data.providers) {
|
||||
const p = typeof r === 'object' ? r : {}
|
||||
db.run(
|
||||
sql,
|
||||
p.id ?? '',
|
||||
p.name ?? '',
|
||||
p.website ?? '',
|
||||
p.contact ?? '',
|
||||
p.baseCurrency ?? '',
|
||||
p.usdRate ?? '',
|
||||
p.eurRate ?? '',
|
||||
p.notes ?? '',
|
||||
p.apiType ?? '',
|
||||
p.apiBaseUrl ?? '',
|
||||
)
|
||||
}
|
||||
}
|
||||
if (Array.isArray(data.providerAccounts) && data.providerAccounts.length > 0) {
|
||||
const sql = `INSERT OR REPLACE INTO provider_accounts (id, providerId, name, panelUrl, currency, billingMode, notes, apiType, apiBaseUrl, apiCredentials) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
||||
for (const r of data.providerAccounts) {
|
||||
const acc = typeof r === 'object' ? r : {}
|
||||
db.run(sql, acc.id ?? '', acc.providerId ?? '', acc.name ?? '', acc.panelUrl ?? '', acc.currency ?? '', acc.billingMode ?? '', acc.notes ?? '', acc.apiType ?? '', acc.apiBaseUrl ?? '', acc.apiCredentials ?? '')
|
||||
}
|
||||
}
|
||||
if (Array.isArray(data.vps) && data.vps.length > 0) {
|
||||
const sql = `INSERT OR REPLACE INTO vps (id, ip, ipv6, additionalIps, dns, providerId, providerAccountId, country, city, datacenter, os, vcpu, ramGb, diskGb, diskType, virtualization, bandwidthTb, sshPort, rootUser, purpose, environment, project, projectId, monitoringEnabled, backupEnabled, status, tariffType, currency, dailyRate, monthlyRate, createdAt, paidUntil, notes, userOverrides) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
||||
for (const r of data.vps) {
|
||||
const v = typeof r === 'object' ? r : {}
|
||||
const additionalIps = Array.isArray(v.additionalIps) ? JSON.stringify(v.additionalIps) : '[]'
|
||||
const dailyRate = v.dailyRate === '' || v.dailyRate == null ? null : Number(v.dailyRate)
|
||||
const monthlyRate = v.monthlyRate === '' || v.monthlyRate == null ? null : Number(v.monthlyRate)
|
||||
const userOverrides = Array.isArray(v.userOverrides) ? JSON.stringify(v.userOverrides) : (v.userOverrides ?? '[]')
|
||||
db.run(sql, v.id ?? '', v.ip ?? '', v.ipv6 ?? '', additionalIps, v.dns ?? '', v.providerId ?? '', v.providerAccountId ?? '', v.country ?? '', v.city ?? '', v.datacenter ?? '', v.os ?? '', v.vcpu ?? 0, v.ramGb ?? 0, v.diskGb ?? 0, v.diskType ?? '', v.virtualization ?? '', v.bandwidthTb ?? 0, v.sshPort ?? 22, v.rootUser ?? '', v.purpose ?? '', v.environment ?? '', v.project ?? '', v.projectId ?? null, v.monitoringEnabled ? 1 : 0, v.backupEnabled ? 1 : 0, v.status ?? 'active', v.tariffType ?? '', v.currency ?? '', dailyRate, monthlyRate, v.createdAt ?? '', v.paidUntil ?? '', v.notes ?? '', userOverrides)
|
||||
}
|
||||
}
|
||||
if (Array.isArray(data.payments) && data.payments.length > 0) {
|
||||
const sql = `INSERT OR REPLACE INTO payments (id, type, date, amount, currency, providerAccountId, vpsId, note) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`
|
||||
for (const r of data.payments) {
|
||||
const pm = typeof r === 'object' ? r : {}
|
||||
db.run(sql, pm.id ?? '', pm.type ?? '', pm.date ?? '', Number(pm.amount) || 0, pm.currency ?? '', pm.providerAccountId ?? '', pm.vpsId ?? '', pm.note ?? '')
|
||||
}
|
||||
}
|
||||
if (Array.isArray(data.balanceLedger) && data.balanceLedger.length > 0) {
|
||||
const sql = `INSERT OR REPLACE INTO balance_ledger (id, type, date, amount, currency, direction, providerAccountId, vpsId, note) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
||||
for (const r of data.balanceLedger) {
|
||||
const bl = typeof r === 'object' ? r : {}
|
||||
db.run(sql, bl.id ?? '', bl.type ?? '', bl.date ?? '', Number(bl.amount) || 0, bl.currency ?? '', bl.direction ?? '', bl.providerAccountId ?? '', bl.vpsId ?? '', bl.note ?? '')
|
||||
}
|
||||
}
|
||||
if (settingsList.length > 0) {
|
||||
const sql = `INSERT OR REPLACE INTO settings (id, baseCurrency, ratesUrl, autoConvert, ratesUpdatedAt, syncEnabled, syncIntervalMinutes) VALUES (?, ?, ?, ?, ?, ?, ?)`
|
||||
for (const r of settingsList) {
|
||||
const s = typeof r === 'object' ? r : {}
|
||||
db.run(sql, s.id ?? 'settings-main', s.baseCurrency ?? 'RUB', s.ratesUrl ?? '', s.autoConvert !== false ? 1 : 0, s.ratesUpdatedAt ?? '', s.syncEnabled ? 1 : 0, s.syncIntervalMinutes ?? 60)
|
||||
}
|
||||
}
|
||||
consolidateAllProviderApiSources(db)
|
||||
saveDb()
|
||||
|
||||
res.json({ ok: true })
|
||||
} catch (err) {
|
||||
console.error('Migrate error:', err)
|
||||
res.status(500).json({ error: err.message || 'Migration failed' })
|
||||
}
|
||||
})
|
||||
|
||||
export default router
|
||||
@@ -0,0 +1,80 @@
|
||||
import { Router } from 'express'
|
||||
import { getDb } from '../db.js'
|
||||
|
||||
const router = Router()
|
||||
|
||||
router.get('/', (req, res) => {
|
||||
try {
|
||||
const db = getDb()
|
||||
const rows = db.prepare('SELECT * FROM payments ORDER BY date DESC').all()
|
||||
res.json(rows)
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message })
|
||||
}
|
||||
})
|
||||
|
||||
router.post('/', (req, res) => {
|
||||
try {
|
||||
const db = getDb()
|
||||
const r = req.body
|
||||
const id = r.id || `pay-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`
|
||||
db.prepare(`
|
||||
INSERT INTO payments (id, type, date, amount, currency, providerAccountId, vpsId, note)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`).run(
|
||||
id,
|
||||
r.type ?? '',
|
||||
r.date ?? '',
|
||||
Number(r.amount) || 0,
|
||||
r.currency ?? '',
|
||||
r.providerAccountId ?? '',
|
||||
r.vpsId ?? '',
|
||||
r.note ?? '',
|
||||
)
|
||||
const row = db.prepare('SELECT * FROM payments WHERE id = ?').get(id)
|
||||
res.status(201).json(row)
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message })
|
||||
}
|
||||
})
|
||||
|
||||
router.put('/:id', (req, res) => {
|
||||
try {
|
||||
const db = getDb()
|
||||
const { id } = req.params
|
||||
const r = req.body
|
||||
db.prepare(`
|
||||
UPDATE payments SET
|
||||
type = ?, date = ?, amount = ?, currency = ?, providerAccountId = ?, vpsId = ?, note = ?
|
||||
WHERE id = ?
|
||||
`).run(
|
||||
r.type ?? '',
|
||||
r.date ?? '',
|
||||
Number(r.amount) || 0,
|
||||
r.currency ?? '',
|
||||
r.providerAccountId ?? '',
|
||||
r.vpsId ?? '',
|
||||
r.note ?? '',
|
||||
id,
|
||||
)
|
||||
const row = db.prepare('SELECT * FROM payments WHERE id = ?').get(id)
|
||||
if (!row) return res.status(404).json({ error: 'Not found' })
|
||||
res.json(row)
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message })
|
||||
}
|
||||
})
|
||||
|
||||
router.delete('/:id', (req, res) => {
|
||||
try {
|
||||
const db = getDb()
|
||||
const { id } = req.params
|
||||
const result = db.prepare('DELETE FROM payments WHERE id = ?').run(id)
|
||||
if (result.changes === 0) return res.status(404).json({ error: 'Not found' })
|
||||
res.status(204).send()
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message })
|
||||
}
|
||||
})
|
||||
|
||||
export default router
|
||||
@@ -0,0 +1,60 @@
|
||||
import { Router } from 'express'
|
||||
import { getDb } from '../db.js'
|
||||
import {
|
||||
normalizeProjectNameInput,
|
||||
projectSuggestions,
|
||||
resolveOrCreateProject,
|
||||
} from '../projects-service.js'
|
||||
|
||||
const router = Router()
|
||||
|
||||
router.get('/', (req, res) => {
|
||||
try {
|
||||
const db = getDb()
|
||||
const rows = db
|
||||
.prepare('SELECT id, name, color, sortOrder, notes, createdAt FROM server_projects ORDER BY name')
|
||||
.all()
|
||||
res.json(rows)
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message })
|
||||
}
|
||||
})
|
||||
|
||||
router.get('/suggest', (req, res) => {
|
||||
try {
|
||||
const db = getDb()
|
||||
const q = req.query.q ?? ''
|
||||
const limit = req.query.limit != null ? Number(req.query.limit) : 20
|
||||
const rows = projectSuggestions(db, q, limit)
|
||||
res.json(rows)
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message })
|
||||
}
|
||||
})
|
||||
|
||||
router.post('/resolve-or-create', (req, res) => {
|
||||
try {
|
||||
const db = getDb()
|
||||
const name = req.body?.name
|
||||
const resolved = resolveOrCreateProject(db, name)
|
||||
res.json(resolved)
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message })
|
||||
}
|
||||
})
|
||||
|
||||
router.post('/', (req, res) => {
|
||||
try {
|
||||
const db = getDb()
|
||||
const name = normalizeProjectNameInput(req.body?.name)
|
||||
if (!name) {
|
||||
return res.status(400).json({ error: 'name is required' })
|
||||
}
|
||||
const resolved = resolveOrCreateProject(db, name)
|
||||
res.status(201).json(resolved)
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message })
|
||||
}
|
||||
})
|
||||
|
||||
export default router
|
||||
@@ -0,0 +1,105 @@
|
||||
import { Router } from 'express'
|
||||
import { getDb } from '../db.js'
|
||||
|
||||
const router = Router()
|
||||
|
||||
function sanitizeAccount(row) {
|
||||
if (!row) return row
|
||||
const { apiCredentials, ...rest } = row
|
||||
return { ...rest, apiCredentialsSet: Boolean(apiCredentials) }
|
||||
}
|
||||
|
||||
router.get('/', (req, res) => {
|
||||
try {
|
||||
const db = getDb()
|
||||
const rows = db.prepare('SELECT * FROM provider_accounts ORDER BY name').all()
|
||||
res.json(rows.map(sanitizeAccount))
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message })
|
||||
}
|
||||
})
|
||||
|
||||
router.post('/', (req, res) => {
|
||||
try {
|
||||
const db = getDb()
|
||||
const r = req.body
|
||||
const id = r.id || `account-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`
|
||||
const alertBelow =
|
||||
r.balance_alert_below != null && r.balance_alert_below !== ''
|
||||
? Number(r.balance_alert_below)
|
||||
: null
|
||||
db.prepare(`
|
||||
INSERT INTO provider_accounts (id, providerId, name, panelUrl, currency, billingMode, notes, apiType, apiBaseUrl, apiCredentials, balance_alert_below)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, '', '', ?, ?)
|
||||
`).run(
|
||||
id,
|
||||
r.providerId ?? '',
|
||||
r.name ?? '',
|
||||
r.panelUrl ?? '',
|
||||
r.currency ?? '',
|
||||
r.billingMode ?? '',
|
||||
r.notes ?? '',
|
||||
r.apiCredentials ?? '',
|
||||
Number.isFinite(alertBelow) ? alertBelow : null,
|
||||
)
|
||||
const row = db.prepare('SELECT * FROM provider_accounts WHERE id = ?').get(id)
|
||||
res.status(201).json(sanitizeAccount(row))
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message })
|
||||
}
|
||||
})
|
||||
|
||||
router.put('/:id', (req, res) => {
|
||||
try {
|
||||
const db = getDb()
|
||||
const { id } = req.params
|
||||
const r = req.body
|
||||
const existing = db.prepare('SELECT * FROM provider_accounts WHERE id = ?').get(id)
|
||||
if (!existing) return res.status(404).json({ error: 'Not found' })
|
||||
const apiCredentials = r.apiCredentials !== undefined ? String(r.apiCredentials || '') : (existing.apiCredentials || '')
|
||||
let balanceAlertBelow = existing.balance_alert_below
|
||||
if (r.balance_alert_below !== undefined) {
|
||||
const v = r.balance_alert_below
|
||||
balanceAlertBelow =
|
||||
v === '' || v == null
|
||||
? null
|
||||
: Number.isFinite(Number(v))
|
||||
? Number(v)
|
||||
: null
|
||||
}
|
||||
db.prepare(`
|
||||
UPDATE provider_accounts SET
|
||||
providerId = ?, name = ?, panelUrl = ?, currency = ?, billingMode = ?, notes = ?,
|
||||
apiType = '', apiBaseUrl = '', apiCredentials = ?, balance_alert_below = ?
|
||||
WHERE id = ?
|
||||
`).run(
|
||||
r.providerId ?? existing.providerId ?? '',
|
||||
r.name ?? existing.name ?? '',
|
||||
r.panelUrl ?? existing.panelUrl ?? '',
|
||||
r.currency ?? existing.currency ?? '',
|
||||
r.billingMode ?? existing.billingMode ?? '',
|
||||
r.notes ?? existing.notes ?? '',
|
||||
apiCredentials,
|
||||
balanceAlertBelow,
|
||||
id,
|
||||
)
|
||||
const row = db.prepare('SELECT * FROM provider_accounts WHERE id = ?').get(id)
|
||||
res.json(sanitizeAccount(row))
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message })
|
||||
}
|
||||
})
|
||||
|
||||
router.delete('/:id', (req, res) => {
|
||||
try {
|
||||
const db = getDb()
|
||||
const { id } = req.params
|
||||
const result = db.prepare('DELETE FROM provider_accounts WHERE id = ?').run(id)
|
||||
if (result.changes === 0) return res.status(404).json({ error: 'Not found' })
|
||||
res.status(204).send()
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message })
|
||||
}
|
||||
})
|
||||
|
||||
export default router
|
||||
@@ -0,0 +1,89 @@
|
||||
import { Router } from 'express'
|
||||
import { getDb } from '../db.js'
|
||||
|
||||
const router = Router()
|
||||
|
||||
router.get('/', (req, res) => {
|
||||
try {
|
||||
const db = getDb()
|
||||
const rows = db.prepare('SELECT * FROM providers ORDER BY name').all()
|
||||
res.json(rows)
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message })
|
||||
}
|
||||
})
|
||||
|
||||
router.post('/', (req, res) => {
|
||||
try {
|
||||
const db = getDb()
|
||||
const r = req.body
|
||||
const id = r.id || `provider-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`
|
||||
db.prepare(`
|
||||
INSERT INTO providers (id, name, website, contact, baseCurrency, usdRate, eurRate, notes, apiType, apiBaseUrl)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`).run(
|
||||
id,
|
||||
r.name ?? '',
|
||||
r.website ?? '',
|
||||
r.contact ?? '',
|
||||
r.baseCurrency ?? '',
|
||||
r.usdRate ?? '',
|
||||
r.eurRate ?? '',
|
||||
r.notes ?? '',
|
||||
r.apiType ?? '',
|
||||
r.apiBaseUrl ?? '',
|
||||
)
|
||||
const row = db.prepare('SELECT * FROM providers WHERE id = ?').get(id)
|
||||
res.status(201).json(row)
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message })
|
||||
}
|
||||
})
|
||||
|
||||
router.put('/:id', (req, res) => {
|
||||
try {
|
||||
const db = getDb()
|
||||
const { id } = req.params
|
||||
const r = req.body
|
||||
const existing = db.prepare('SELECT * FROM providers WHERE id = ?').get(id)
|
||||
if (!existing) return res.status(404).json({ error: 'Not found' })
|
||||
const apiType = r.apiType !== undefined ? String(r.apiType || '') : (existing.apiType || '')
|
||||
const apiBaseUrl =
|
||||
r.apiBaseUrl !== undefined ? String(r.apiBaseUrl || '') : (existing.apiBaseUrl || '')
|
||||
db.prepare(`
|
||||
UPDATE providers SET
|
||||
name = ?, website = ?, contact = ?, baseCurrency = ?, usdRate = ?, eurRate = ?, notes = ?,
|
||||
apiType = ?, apiBaseUrl = ?
|
||||
WHERE id = ?
|
||||
`).run(
|
||||
r.name ?? '',
|
||||
r.website ?? '',
|
||||
r.contact ?? '',
|
||||
r.baseCurrency ?? '',
|
||||
r.usdRate ?? '',
|
||||
r.eurRate ?? '',
|
||||
r.notes ?? '',
|
||||
apiType,
|
||||
apiBaseUrl,
|
||||
id,
|
||||
)
|
||||
const row = db.prepare('SELECT * FROM providers WHERE id = ?').get(id)
|
||||
res.json(row)
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message })
|
||||
}
|
||||
})
|
||||
|
||||
router.delete('/:id', (req, res) => {
|
||||
try {
|
||||
const db = getDb()
|
||||
const { id } = req.params
|
||||
const result = db.prepare('DELETE FROM providers WHERE id = ?').run(id)
|
||||
if (result.changes === 0) return res.status(404).json({ error: 'Not found' })
|
||||
res.status(204).send()
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message })
|
||||
}
|
||||
})
|
||||
|
||||
export default router
|
||||
@@ -0,0 +1,22 @@
|
||||
import { Router } from 'express'
|
||||
|
||||
const router = Router()
|
||||
|
||||
router.get('/', async (req, res) => {
|
||||
const url = req.query.url
|
||||
if (!url) {
|
||||
return res.status(400).json({ error: 'Missing url parameter' })
|
||||
}
|
||||
try {
|
||||
const response = await fetch(url, { signal: AbortSignal.timeout(10_000) })
|
||||
if (!response.ok) {
|
||||
return res.status(502).json({ error: `Upstream returned ${response.status}` })
|
||||
}
|
||||
const data = await response.json()
|
||||
res.json(data)
|
||||
} catch (err) {
|
||||
res.status(502).json({ error: err.message || 'Failed to fetch rates' })
|
||||
}
|
||||
})
|
||||
|
||||
export default router
|
||||
@@ -0,0 +1,182 @@
|
||||
import { Router } from 'express'
|
||||
import { getDb } from '../db.js'
|
||||
import { startScheduler } from '../sync-scheduler.js'
|
||||
import { sendTelegramMessage } from '../telegram.js'
|
||||
|
||||
const router = Router()
|
||||
|
||||
export function rowToSettings(row) {
|
||||
if (!row) return null
|
||||
let customFields = []
|
||||
if (row.customFields) {
|
||||
try {
|
||||
customFields = JSON.parse(row.customFields)
|
||||
} catch {
|
||||
customFields = []
|
||||
}
|
||||
}
|
||||
const { telegramBotToken, ...rest } = row
|
||||
return {
|
||||
...rest,
|
||||
telegramBotTokenSet: Boolean(telegramBotToken?.trim()),
|
||||
autoConvert: Boolean(row.autoConvert),
|
||||
syncEnabled: Boolean(row.syncEnabled),
|
||||
notifyPaymentExpiryEnabled: Boolean(row.notifyPaymentExpiryEnabled),
|
||||
notifyNewTariffsEnabled: Boolean(row.notifyNewTariffsEnabled),
|
||||
notifyLowBalanceEnabled: Boolean(row.notifyLowBalanceEnabled),
|
||||
notifySyncDigestEnabled: Boolean(row.notifySyncDigestEnabled),
|
||||
customFields: Array.isArray(customFields) ? customFields : [],
|
||||
}
|
||||
}
|
||||
|
||||
router.post('/telegram/test', async (req, res) => {
|
||||
try {
|
||||
const db = getDb()
|
||||
const row = db.prepare('SELECT telegramBotToken, telegramChatId, telegramMessageThreadId FROM settings WHERE id = ?').get('settings-main')
|
||||
if (!row?.telegramBotToken?.trim() || !row?.telegramChatId?.trim()) {
|
||||
return res.status(400).json({ ok: false, error: 'Укажите токен бота и Chat ID в настройках' })
|
||||
}
|
||||
const text = '✅ <b>Тестовое уведомление</b>\n\nVPS Tracker — уведомления настроены корректно.'
|
||||
await sendTelegramMessage(row.telegramBotToken, row.telegramChatId, text, row.telegramMessageThreadId || undefined)
|
||||
res.json({ ok: true })
|
||||
} catch (err) {
|
||||
res.status(500).json({ ok: false, error: err.message || 'Ошибка отправки' })
|
||||
}
|
||||
})
|
||||
|
||||
router.get('/', (req, res) => {
|
||||
try {
|
||||
const db = getDb()
|
||||
const rows = db.prepare('SELECT * FROM settings ORDER BY id').all()
|
||||
res.json(rows.map(rowToSettings))
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message })
|
||||
}
|
||||
})
|
||||
|
||||
function serializeCustomFields(val) {
|
||||
if (val == null) return null
|
||||
if (Array.isArray(val)) return JSON.stringify(val)
|
||||
if (typeof val === 'string') return val || null
|
||||
return null
|
||||
}
|
||||
|
||||
router.put('/:id', (req, res) => {
|
||||
try {
|
||||
const db = getDb()
|
||||
const { id } = req.params
|
||||
const r = req.body
|
||||
const existing = db.prepare('SELECT * FROM settings WHERE id = ?').get(id)
|
||||
const syncEnabled = r.syncEnabled !== undefined ? (r.syncEnabled ? 1 : 0) : (existing?.syncEnabled ? 1 : 0)
|
||||
const syncIntervalMinutes = r.syncIntervalMinutes !== undefined ? Math.max(15, Number(r.syncIntervalMinutes) || 60) : (existing?.syncIntervalMinutes ?? 60)
|
||||
const syncTariffsIntervalMinutes = r.syncTariffsIntervalMinutes !== undefined ? Math.max(60, Number(r.syncTariffsIntervalMinutes) || 1440) : (existing?.syncTariffsIntervalMinutes ?? 1440)
|
||||
const notifyPaymentExpiryEnabled = r.notifyPaymentExpiryEnabled !== undefined ? (r.notifyPaymentExpiryEnabled ? 1 : 0) : (existing?.notifyPaymentExpiryEnabled ? 1 : 0)
|
||||
const notifyNewTariffsEnabled = r.notifyNewTariffsEnabled !== undefined ? (r.notifyNewTariffsEnabled ? 1 : 0) : (existing?.notifyNewTariffsEnabled ? 1 : 0)
|
||||
const notifyLowBalanceEnabled = r.notifyLowBalanceEnabled !== undefined ? (r.notifyLowBalanceEnabled ? 1 : 0) : (existing?.notifyLowBalanceEnabled ? 1 : 0)
|
||||
const notifySyncDigestEnabled = r.notifySyncDigestEnabled !== undefined ? (r.notifySyncDigestEnabled ? 1 : 0) : (existing?.notifySyncDigestEnabled ? 1 : 0)
|
||||
const telegramBotToken = r.telegramBotToken !== undefined ? (r.telegramBotToken || '') : (existing?.telegramBotToken ?? '')
|
||||
const telegramChatId = r.telegramChatId !== undefined ? (r.telegramChatId || '') : (existing?.telegramChatId ?? '')
|
||||
const telegramMessageThreadId = r.telegramMessageThreadId !== undefined ? (r.telegramMessageThreadId || '') : (existing?.telegramMessageThreadId ?? '')
|
||||
const customFields = serializeCustomFields(r.customFields ?? existing?.customFields)
|
||||
if (existing) {
|
||||
db.prepare(`
|
||||
UPDATE settings SET
|
||||
baseCurrency = ?, ratesUrl = ?, autoConvert = ?, ratesUpdatedAt = ?, syncEnabled = ?, syncIntervalMinutes = ?, syncTariffsIntervalMinutes = ?,
|
||||
telegramBotToken = ?, telegramChatId = ?, telegramMessageThreadId = ?, notifyPaymentExpiryEnabled = ?, notifyNewTariffsEnabled = ?, customFields = ?,
|
||||
notifyLowBalanceEnabled = ?, notifySyncDigestEnabled = ?
|
||||
WHERE id = ?
|
||||
`).run(
|
||||
r.baseCurrency ?? existing.baseCurrency ?? 'RUB',
|
||||
r.ratesUrl ?? existing.ratesUrl ?? '',
|
||||
r.autoConvert !== undefined ? (r.autoConvert !== false ? 1 : 0) : (existing.autoConvert ? 1 : 0),
|
||||
r.ratesUpdatedAt ?? existing.ratesUpdatedAt ?? '',
|
||||
syncEnabled,
|
||||
syncIntervalMinutes,
|
||||
syncTariffsIntervalMinutes,
|
||||
telegramBotToken,
|
||||
telegramChatId,
|
||||
telegramMessageThreadId,
|
||||
notifyPaymentExpiryEnabled,
|
||||
notifyNewTariffsEnabled,
|
||||
customFields,
|
||||
notifyLowBalanceEnabled,
|
||||
notifySyncDigestEnabled,
|
||||
id,
|
||||
)
|
||||
} else {
|
||||
db.prepare(`
|
||||
INSERT INTO settings (id, baseCurrency, ratesUrl, autoConvert, ratesUpdatedAt, syncEnabled, syncIntervalMinutes, syncTariffsIntervalMinutes, telegramBotToken, telegramChatId, telegramMessageThreadId, notifyPaymentExpiryEnabled, notifyNewTariffsEnabled, customFields, notifyLowBalanceEnabled, notifySyncDigestEnabled)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`).run(
|
||||
id,
|
||||
r.baseCurrency ?? 'RUB',
|
||||
r.ratesUrl ?? '',
|
||||
r.autoConvert !== false ? 1 : 0,
|
||||
r.ratesUpdatedAt ?? '',
|
||||
syncEnabled,
|
||||
syncIntervalMinutes,
|
||||
syncTariffsIntervalMinutes,
|
||||
telegramBotToken,
|
||||
telegramChatId,
|
||||
telegramMessageThreadId,
|
||||
notifyPaymentExpiryEnabled,
|
||||
notifyNewTariffsEnabled,
|
||||
customFields,
|
||||
notifyLowBalanceEnabled,
|
||||
notifySyncDigestEnabled,
|
||||
)
|
||||
}
|
||||
startScheduler()
|
||||
const row = db.prepare('SELECT * FROM settings WHERE id = ?').get(id)
|
||||
res.json(rowToSettings(row))
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message })
|
||||
}
|
||||
})
|
||||
|
||||
router.post('/', (req, res) => {
|
||||
try {
|
||||
const db = getDb()
|
||||
const r = req.body
|
||||
const id = r.id ?? 'settings-main'
|
||||
const syncEnabled = r.syncEnabled ? 1 : 0
|
||||
const syncIntervalMinutes = Math.max(15, Number(r.syncIntervalMinutes) || 60)
|
||||
const syncTariffsIntervalMinutes = Math.max(60, Number(r.syncTariffsIntervalMinutes) || 1440)
|
||||
const notifyPaymentExpiryEnabled = r.notifyPaymentExpiryEnabled ? 1 : 0
|
||||
const notifyNewTariffsEnabled = r.notifyNewTariffsEnabled ? 1 : 0
|
||||
const notifyLowBalanceEnabled = r.notifyLowBalanceEnabled ? 1 : 0
|
||||
const notifySyncDigestEnabled = r.notifySyncDigestEnabled ? 1 : 0
|
||||
const telegramBotToken = r.telegramBotToken ?? ''
|
||||
const telegramChatId = r.telegramChatId ?? ''
|
||||
const telegramMessageThreadId = r.telegramMessageThreadId ?? ''
|
||||
const customFields = serializeCustomFields(r.customFields)
|
||||
db.prepare(`
|
||||
INSERT INTO settings (id, baseCurrency, ratesUrl, autoConvert, ratesUpdatedAt, syncEnabled, syncIntervalMinutes, syncTariffsIntervalMinutes, telegramBotToken, telegramChatId, telegramMessageThreadId, notifyPaymentExpiryEnabled, notifyNewTariffsEnabled, customFields, notifyLowBalanceEnabled, notifySyncDigestEnabled)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`).run(
|
||||
id,
|
||||
r.baseCurrency ?? 'RUB',
|
||||
r.ratesUrl ?? 'https://www.cbr-xml-daily.ru/latest.js',
|
||||
r.autoConvert !== false ? 1 : 0,
|
||||
r.ratesUpdatedAt ?? '',
|
||||
syncEnabled,
|
||||
syncIntervalMinutes,
|
||||
syncTariffsIntervalMinutes,
|
||||
telegramBotToken,
|
||||
telegramChatId,
|
||||
telegramMessageThreadId,
|
||||
notifyPaymentExpiryEnabled,
|
||||
notifyNewTariffsEnabled,
|
||||
customFields,
|
||||
notifyLowBalanceEnabled,
|
||||
notifySyncDigestEnabled,
|
||||
)
|
||||
startScheduler()
|
||||
const row = db.prepare('SELECT * FROM settings WHERE id = ?').get(id)
|
||||
res.status(201).json(rowToSettings(row))
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message })
|
||||
}
|
||||
})
|
||||
|
||||
export default router
|
||||
@@ -0,0 +1,120 @@
|
||||
import { Router } from 'express'
|
||||
import { getDb } from '../db.js'
|
||||
import { fetchDashboardInfo, testConnection } from '../adapters/billmanager/index.js'
|
||||
import { runBillmanagerAccountSync } from '../sync-account-job.js'
|
||||
import { billmanagerAccountRowForSync } from '../utils/billmanager-context.js'
|
||||
|
||||
const router = Router()
|
||||
|
||||
router.post('/test-connection', async (req, res) => {
|
||||
try {
|
||||
const { apiBaseUrl, apiCredentials } = req.body || {}
|
||||
if (!apiBaseUrl?.trim() || !apiCredentials?.trim()) {
|
||||
return res.status(400).json({ ok: false, error: 'Укажите URL и учётные данные' })
|
||||
}
|
||||
const result = await testConnection(apiBaseUrl.trim(), apiCredentials.trim())
|
||||
res.json(result)
|
||||
} catch (err) {
|
||||
res.status(500).json({ ok: false, error: err.message || 'Ошибка проверки' })
|
||||
}
|
||||
})
|
||||
|
||||
router.get('/status', (req, res) => {
|
||||
try {
|
||||
const db = getDb()
|
||||
const rows = db.prepare(`
|
||||
SELECT id, accountId, startedAt, finishedAt, status, vpsCount, paymentsCount, error, summary
|
||||
FROM sync_log ORDER BY startedAt DESC LIMIT 50
|
||||
`).all()
|
||||
res.json(
|
||||
rows.map((row) => {
|
||||
let summaryParsed = null
|
||||
if (row.summary) {
|
||||
try {
|
||||
summaryParsed = JSON.parse(row.summary)
|
||||
} catch {
|
||||
summaryParsed = null
|
||||
}
|
||||
}
|
||||
return { ...row, summary: summaryParsed }
|
||||
}),
|
||||
)
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message })
|
||||
}
|
||||
})
|
||||
|
||||
router.get('/:accountId/balance', async (req, res) => {
|
||||
try {
|
||||
const db = getDb()
|
||||
const { accountId } = req.params
|
||||
const row = db.prepare('SELECT * FROM provider_accounts WHERE id = ?').get(accountId)
|
||||
if (!row) {
|
||||
return res.status(404).json({ error: 'Account not found' })
|
||||
}
|
||||
const provider = row.providerId
|
||||
? db.prepare('SELECT * FROM providers WHERE id = ?').get(row.providerId)
|
||||
: null
|
||||
const syncRow = billmanagerAccountRowForSync(row, provider)
|
||||
if (!syncRow) {
|
||||
return res.status(400).json({
|
||||
error:
|
||||
'Укажите в настройках хостера тип API BILLmanager и URL; в аккаунте — логин и пароль API',
|
||||
})
|
||||
}
|
||||
const info = await fetchDashboardInfo(syncRow.apiBaseUrl, syncRow.apiCredentials.trim(), {
|
||||
fallbackCurrency: row.currency,
|
||||
})
|
||||
db.run(
|
||||
'UPDATE provider_accounts SET balance_api=?, balance_currency=?, balance_updated_at=?, enoughmoneyto=? WHERE id=?',
|
||||
info.balance,
|
||||
info.currency || 'RUB',
|
||||
new Date().toISOString(),
|
||||
info.enoughmoneyto || '',
|
||||
accountId,
|
||||
)
|
||||
res.json({ ok: true, balance: info })
|
||||
} catch (err) {
|
||||
console.error('Balance fetch error:', err)
|
||||
res.status(500).json({ ok: false, error: err.message || 'Failed to fetch balance' })
|
||||
}
|
||||
})
|
||||
|
||||
router.post('/:accountId', async (req, res) => {
|
||||
try {
|
||||
const db = getDb()
|
||||
const { accountId } = req.params
|
||||
const { onlyTariffs = false } = req.body || {}
|
||||
const row = db.prepare('SELECT * FROM provider_accounts WHERE id = ?').get(accountId)
|
||||
if (!row) {
|
||||
return res.status(404).json({ error: 'Account not found' })
|
||||
}
|
||||
const provider = row.providerId
|
||||
? db.prepare('SELECT * FROM providers WHERE id = ?').get(row.providerId)
|
||||
: null
|
||||
const syncRow = billmanagerAccountRowForSync(row, provider)
|
||||
if (!syncRow) {
|
||||
return res.status(400).json({
|
||||
error:
|
||||
'Укажите в настройках хостера тип API BILLmanager и URL; в аккаунте — логин и пароль API',
|
||||
})
|
||||
}
|
||||
|
||||
const opts = onlyTariffs ? { skipVpsPayments: true } : { skipTariffs: true }
|
||||
const result = await runBillmanagerAccountSync(syncRow, opts)
|
||||
|
||||
res.json({
|
||||
ok: true,
|
||||
synced: {
|
||||
vpsCount: result.vpsCount,
|
||||
paymentsCount: result.paymentsCount,
|
||||
tariffsCount: result.tariffsCount ?? 0,
|
||||
},
|
||||
})
|
||||
} catch (err) {
|
||||
console.error('Sync error:', err)
|
||||
res.status(500).json({ ok: false, error: err.message || 'Sync failed' })
|
||||
}
|
||||
})
|
||||
|
||||
export default router
|
||||
@@ -0,0 +1,304 @@
|
||||
import { Router } from 'express'
|
||||
import { getDb } from '../db.js'
|
||||
import { resolveOrCreateProject } from '../projects-service.js'
|
||||
|
||||
const router = Router()
|
||||
|
||||
function projectColumnsForSave(db, projectInput) {
|
||||
const resolved = resolveOrCreateProject(db, projectInput)
|
||||
if (!resolved.id) {
|
||||
return { project: '', projectId: '' }
|
||||
}
|
||||
return { project: resolved.name, projectId: resolved.id }
|
||||
}
|
||||
|
||||
export function rowToVps(row) {
|
||||
if (!row) return null
|
||||
let additionalIps = []
|
||||
try {
|
||||
additionalIps = row.additionalIps ? JSON.parse(row.additionalIps) : []
|
||||
} catch {
|
||||
additionalIps = []
|
||||
}
|
||||
let userOverrides = []
|
||||
try {
|
||||
userOverrides = row.userOverrides ? JSON.parse(row.userOverrides) : []
|
||||
} catch {
|
||||
userOverrides = []
|
||||
}
|
||||
return {
|
||||
...row,
|
||||
additionalIps,
|
||||
userOverrides,
|
||||
projectId: row.projectId ?? '',
|
||||
monitoringEnabled: Boolean(row.monitoringEnabled),
|
||||
backupEnabled: Boolean(row.backupEnabled),
|
||||
dailyRate: row.dailyRate != null ? row.dailyRate : '',
|
||||
monthlyRate: row.monthlyRate != null ? row.monthlyRate : '',
|
||||
}
|
||||
}
|
||||
|
||||
router.get('/', (req, res) => {
|
||||
try {
|
||||
const db = getDb()
|
||||
const rows = db.prepare('SELECT * FROM vps ORDER BY createdAt DESC').all()
|
||||
res.json(rows.map(rowToVps))
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message })
|
||||
}
|
||||
})
|
||||
|
||||
router.post('/', (req, res) => {
|
||||
try {
|
||||
const db = getDb()
|
||||
const r = req.body
|
||||
const id = r.id || `vps-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`
|
||||
const additionalIps = Array.isArray(r.additionalIps) ? JSON.stringify(r.additionalIps) : '[]'
|
||||
const dailyRate = r.dailyRate === '' || r.dailyRate == null ? null : Number(r.dailyRate)
|
||||
const monthlyRate = r.monthlyRate === '' || r.monthlyRate == null ? null : Number(r.monthlyRate)
|
||||
const { project, projectId } = projectColumnsForSave(db, r.project)
|
||||
|
||||
db.prepare(`
|
||||
INSERT INTO vps (
|
||||
id, ip, ipv6, additionalIps, dns, providerId, providerAccountId, country, city, datacenter,
|
||||
os, vcpu, ramGb, diskGb, diskType, virtualization, bandwidthTb, sshPort, rootUser,
|
||||
purpose, environment, project, projectId, monitoringEnabled, backupEnabled, status, tariffType,
|
||||
currency, dailyRate, monthlyRate, createdAt, paidUntil, notes, userOverrides
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`).run(
|
||||
id,
|
||||
r.ip ?? '',
|
||||
r.ipv6 ?? '',
|
||||
additionalIps,
|
||||
r.dns ?? '',
|
||||
r.providerId ?? '',
|
||||
r.providerAccountId ?? '',
|
||||
r.country ?? '',
|
||||
r.city ?? '',
|
||||
r.datacenter ?? '',
|
||||
r.os ?? '',
|
||||
r.vcpu ?? 0,
|
||||
r.ramGb ?? 0,
|
||||
r.diskGb ?? 0,
|
||||
r.diskType ?? '',
|
||||
r.virtualization ?? '',
|
||||
r.bandwidthTb ?? 0,
|
||||
r.sshPort ?? 22,
|
||||
r.rootUser ?? '',
|
||||
r.purpose ?? '',
|
||||
r.environment ?? '',
|
||||
project,
|
||||
projectId || null,
|
||||
r.monitoringEnabled ? 1 : 0,
|
||||
r.backupEnabled ? 1 : 0,
|
||||
r.status ?? 'active',
|
||||
r.tariffType ?? '',
|
||||
r.currency ?? '',
|
||||
dailyRate,
|
||||
monthlyRate,
|
||||
r.createdAt ?? new Date().toISOString().slice(0, 10),
|
||||
r.paidUntil ?? '',
|
||||
r.notes ?? '',
|
||||
r.userOverrides ? (Array.isArray(r.userOverrides) ? JSON.stringify(r.userOverrides) : r.userOverrides) : '[]',
|
||||
)
|
||||
const row = db.prepare('SELECT * FROM vps WHERE id = ?').get(id)
|
||||
res.status(201).json(rowToVps(row))
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message })
|
||||
}
|
||||
})
|
||||
|
||||
const USER_OVERRIDABLE_FIELDS = ['country', 'city', 'datacenter', 'os', 'vcpu', 'ramGb', 'diskGb', 'diskType', 'virtualization', 'purpose', 'environment', 'project', 'notes', 'sshPort', 'rootUser', 'bandwidthTb', 'monitoringEnabled', 'backupEnabled']
|
||||
|
||||
router.put('/:id', (req, res) => {
|
||||
try {
|
||||
const db = getDb()
|
||||
const { id } = req.params
|
||||
const r = req.body
|
||||
const existing = db.prepare('SELECT * FROM vps WHERE id = ?').get(id)
|
||||
if (!existing) return res.status(404).json({ error: 'Not found' })
|
||||
|
||||
let userOverrides = []
|
||||
try {
|
||||
userOverrides = existing.userOverrides ? JSON.parse(existing.userOverrides) : []
|
||||
} catch {
|
||||
userOverrides = []
|
||||
}
|
||||
const clearOverrides =
|
||||
r.userOverrides === 'clear' || (Array.isArray(r.userOverrides) && r.userOverrides.length === 0)
|
||||
if (clearOverrides) {
|
||||
userOverrides = []
|
||||
}
|
||||
|
||||
const additionalIps = Array.isArray(r.additionalIps) ? JSON.stringify(r.additionalIps) : '[]'
|
||||
const dailyRate = r.dailyRate === '' || r.dailyRate == null ? null : Number(r.dailyRate)
|
||||
const monthlyRate = r.monthlyRate === '' || r.monthlyRate == null ? null : Number(r.monthlyRate)
|
||||
|
||||
let projectOut = existing.project ?? ''
|
||||
let projectIdOut = existing.projectId ?? ''
|
||||
if (r.project !== undefined) {
|
||||
const resolved = projectColumnsForSave(db, r.project)
|
||||
projectOut = resolved.project
|
||||
projectIdOut = resolved.projectId
|
||||
} else if (r.projectId !== undefined) {
|
||||
if (!r.projectId) {
|
||||
projectOut = ''
|
||||
projectIdOut = ''
|
||||
} else {
|
||||
const prow = db.prepare('SELECT name FROM server_projects WHERE id = ?').get(r.projectId)
|
||||
projectOut = prow?.name ?? ''
|
||||
projectIdOut = r.projectId
|
||||
}
|
||||
}
|
||||
|
||||
if (!clearOverrides) {
|
||||
for (const f of USER_OVERRIDABLE_FIELDS) {
|
||||
if (f === 'project') {
|
||||
const projectChanged =
|
||||
String(projectOut ?? '') !== String(existing.project ?? '') ||
|
||||
String(projectIdOut ?? '') !== String(existing.projectId ?? '')
|
||||
if (projectChanged && !userOverrides.includes('project')) {
|
||||
userOverrides.push('project')
|
||||
}
|
||||
continue
|
||||
}
|
||||
const newVal = r[f]
|
||||
const oldVal = existing[f]
|
||||
const changed = String(newVal ?? '') !== String(oldVal ?? '')
|
||||
if (changed && !userOverrides.includes(f)) {
|
||||
userOverrides.push(f)
|
||||
}
|
||||
}
|
||||
}
|
||||
const userOverridesJson = JSON.stringify([...new Set(userOverrides)])
|
||||
|
||||
db.prepare(`
|
||||
UPDATE vps SET
|
||||
ip = ?, ipv6 = ?, additionalIps = ?, dns = ?, providerId = ?, providerAccountId = ?,
|
||||
country = ?, city = ?, datacenter = ?, os = ?, vcpu = ?, ramGb = ?, diskGb = ?, diskType = ?,
|
||||
virtualization = ?, bandwidthTb = ?, sshPort = ?, rootUser = ?, purpose = ?, environment = ?,
|
||||
project = ?, projectId = ?, monitoringEnabled = ?, backupEnabled = ?, status = ?, tariffType = ?,
|
||||
currency = ?, dailyRate = ?, monthlyRate = ?, createdAt = ?, paidUntil = ?, notes = ?,
|
||||
userOverrides = ?
|
||||
WHERE id = ?
|
||||
`).run(
|
||||
r.ip ?? '',
|
||||
r.ipv6 ?? '',
|
||||
additionalIps,
|
||||
r.dns ?? '',
|
||||
r.providerId ?? '',
|
||||
r.providerAccountId ?? '',
|
||||
r.country ?? '',
|
||||
r.city ?? '',
|
||||
r.datacenter ?? '',
|
||||
r.os ?? '',
|
||||
r.vcpu ?? 0,
|
||||
r.ramGb ?? 0,
|
||||
r.diskGb ?? 0,
|
||||
r.diskType ?? '',
|
||||
r.virtualization ?? '',
|
||||
r.bandwidthTb ?? 0,
|
||||
r.sshPort ?? 22,
|
||||
r.rootUser ?? '',
|
||||
r.purpose ?? '',
|
||||
r.environment ?? '',
|
||||
projectOut,
|
||||
projectIdOut || null,
|
||||
r.monitoringEnabled ? 1 : 0,
|
||||
r.backupEnabled ? 1 : 0,
|
||||
r.status ?? 'active',
|
||||
r.tariffType ?? '',
|
||||
r.currency ?? '',
|
||||
dailyRate,
|
||||
monthlyRate,
|
||||
r.createdAt ?? '',
|
||||
r.paidUntil ?? '',
|
||||
r.notes ?? '',
|
||||
userOverridesJson,
|
||||
id,
|
||||
)
|
||||
const row = db.prepare('SELECT * FROM vps WHERE id = ?').get(id)
|
||||
if (!row) return res.status(404).json({ error: 'Not found' })
|
||||
res.json(rowToVps(row))
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message })
|
||||
}
|
||||
})
|
||||
|
||||
router.delete('/:id', (req, res) => {
|
||||
try {
|
||||
const db = getDb()
|
||||
const { id } = req.params
|
||||
const result = db.prepare('DELETE FROM vps WHERE id = ?').run(id)
|
||||
if (result.changes === 0) return res.status(404).json({ error: 'Not found' })
|
||||
res.status(204).send()
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message })
|
||||
}
|
||||
})
|
||||
|
||||
router.patch('/bulk', (req, res) => {
|
||||
try {
|
||||
const db = getDb()
|
||||
const { ids = [], action, value } = req.body
|
||||
if (!Array.isArray(ids) || ids.length === 0) {
|
||||
return res.status(400).json({ error: 'ids must be a non-empty array' })
|
||||
}
|
||||
if (action === 'status' && value) {
|
||||
const validStatus = ['active', 'paused', 'archived']
|
||||
if (!validStatus.includes(value)) {
|
||||
return res.status(400).json({ error: 'value must be active, paused, or archived' })
|
||||
}
|
||||
const stmt = db.prepare('UPDATE vps SET status = ? WHERE id = ?')
|
||||
for (const id of ids) {
|
||||
stmt.run(value, id)
|
||||
}
|
||||
return res.json({ updated: ids.length, status: value })
|
||||
}
|
||||
if (action === 'delete') {
|
||||
const stmt = db.prepare('DELETE FROM vps WHERE id = ?')
|
||||
let deleted = 0
|
||||
for (const id of ids) {
|
||||
const result = stmt.run(id)
|
||||
if (result.changes > 0) deleted++
|
||||
}
|
||||
return res.json({ deleted })
|
||||
}
|
||||
if (action === 'project') {
|
||||
const projectValue = value == null ? '' : String(value)
|
||||
const { project: projName, projectId: projId } = projectColumnsForSave(db, projectValue)
|
||||
const getStmt = db.prepare('SELECT * FROM vps WHERE id = ?')
|
||||
const updStmt = db.prepare(
|
||||
'UPDATE vps SET project = ?, projectId = ?, userOverrides = ? WHERE id = ?',
|
||||
)
|
||||
let updated = 0
|
||||
for (const id of ids) {
|
||||
const existing = getStmt.get(id)
|
||||
if (!existing) continue
|
||||
if (
|
||||
String(existing.project ?? '') === projName &&
|
||||
String(existing.projectId ?? '') === String(projId ?? '')
|
||||
) {
|
||||
continue
|
||||
}
|
||||
let userOverrides = []
|
||||
try {
|
||||
userOverrides = existing.userOverrides ? JSON.parse(existing.userOverrides) : []
|
||||
} catch {
|
||||
userOverrides = []
|
||||
}
|
||||
if (!userOverrides.includes('project')) {
|
||||
userOverrides.push('project')
|
||||
}
|
||||
updStmt.run(projName, projId || null, JSON.stringify([...new Set(userOverrides)]), id)
|
||||
updated++
|
||||
}
|
||||
return res.json({ updated, project: projName, projectId: projId })
|
||||
}
|
||||
return res.status(400).json({ error: 'action must be status, delete, or project' })
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message })
|
||||
}
|
||||
})
|
||||
|
||||
export default router
|
||||
Reference in New Issue
Block a user